riffer-rig 0.8.0 → 0.9.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/README.md +11 -2
  4. data/lib/riffer/rig/cli/flags.rb +16 -2
  5. data/lib/riffer/rig/cli.rb +2 -1
  6. data/lib/riffer/rig/commands/auth.rb +79 -0
  7. data/lib/riffer/rig/commands/model.rb +70 -21
  8. data/lib/riffer/rig/commands/reload.rb +23 -0
  9. data/lib/riffer/rig/credentials.rb +13 -1
  10. data/lib/riffer/rig/extension.rb +12 -0
  11. data/lib/riffer/rig/loader.rb +258 -31
  12. data/lib/riffer/rig/provider_setup.rb +36 -5
  13. data/lib/riffer/rig/providers.rb +48 -0
  14. data/lib/riffer/rig/registrar.rb +8 -0
  15. data/lib/riffer/rig/runtime.rb +110 -21
  16. data/lib/riffer/rig/sdk.rb +62 -0
  17. data/lib/riffer/rig/settings/document.rb +7 -1
  18. data/lib/riffer/rig/settings.rb +5 -4
  19. data/lib/riffer/rig/stores/jsonl.rb +115 -0
  20. data/lib/riffer/rig/stores/recorder.rb +119 -0
  21. data/lib/riffer/rig/version.rb +1 -1
  22. data/lib/riffer/rig.rb +7 -1
  23. data/sig/generated/riffer/rig/cli/flags.rbs +5 -2
  24. data/sig/generated/riffer/rig/commands/auth.rbs +35 -0
  25. data/sig/generated/riffer/rig/commands/model.rbs +21 -9
  26. data/sig/generated/riffer/rig/commands/reload.rbs +12 -0
  27. data/sig/generated/riffer/rig/credentials.rbs +8 -0
  28. data/sig/generated/riffer/rig/extension.rbs +4 -0
  29. data/sig/generated/riffer/rig/loader.rbs +108 -7
  30. data/sig/generated/riffer/rig/provider_setup.rbs +15 -4
  31. data/sig/generated/riffer/rig/providers.rbs +28 -0
  32. data/sig/generated/riffer/rig/registrar.rbs +6 -0
  33. data/sig/generated/riffer/rig/runtime.rbs +63 -9
  34. data/sig/generated/riffer/rig/sdk.rbs +33 -0
  35. data/sig/generated/riffer/rig/settings/document.rbs +5 -1
  36. data/sig/generated/riffer/rig/settings.rbs +2 -3
  37. data/sig/generated/riffer/rig/stores/jsonl.rbs +64 -0
  38. data/sig/generated/riffer/rig/stores/recorder.rbs +67 -0
  39. data/sig/generated/riffer/rig.rbs +4 -0
  40. data/sig/manual/riffer/rig/commands/auth.rbs +6 -0
  41. data/sig/manual/riffer/rig/commands/reload.rbs +6 -0
  42. data/sig/manual/riffer/rig/providers.rbs +6 -0
  43. data/sig/manual/riffer/rig/sdk.rbs +6 -0
  44. data/sig/manual/riffer/rig/stores.rbs +11 -0
  45. metadata +18 -29
@@ -3,24 +3,24 @@
3
3
  class Riffer::Rig::Loader
4
4
  class ConfigurationError < StandardError; end
5
5
 
6
- PROVIDER_LIST = Riffer::Rig::Settings::PROVIDERS.join(', ') #: String
7
-
8
- ONBOARDING_QUESTION = 'Which model should riffer use? Enter provider/name, ' \
9
- "with a provider from: #{PROVIDER_LIST}".freeze #: String
10
-
11
- NO_MODEL = 'No model is set: pass --model provider/name, set RIFFER_MODEL, or set "model" in ' \
12
- "~/.riffer/settings.json, with a provider from: #{PROVIDER_LIST}".freeze #: String
6
+ # Raised by a strict load: the reload abandons instead of continuing.
7
+ class AbandonedError < StandardError; end
13
8
 
14
9
  EXTENSION_FEATURE = 'riffer/rig/extension' #: String
15
10
 
16
- private_constant :EXTENSION_FEATURE, :PROVIDER_LIST, :ONBOARDING_QUESTION, :NO_MODEL
11
+ private_constant :EXTENSION_FEATURE
17
12
 
18
13
  # @rbs @cwd: String
19
14
  # @rbs @host: Riffer::Rig::Hosts::_Host
20
15
  # @rbs @env: Riffer::Rig::Env
21
16
  # @rbs @home: String
22
17
  # @rbs @riffer_config: Riffer::Config
18
+ # @rbs @store: Riffer::Rig::Stores::_Store | nil
23
19
  # @rbs @tracked_files: Array[String]
20
+ # @rbs @stripped: Array[Symbol]
21
+ # @rbs @extensions_enabled: bool
22
+ # @rbs @gem_extensions: Array[Riffer::Rig::Extension]
23
+ # @rbs @file_state: Hash[String, Time?]?
24
24
 
25
25
  # @dynamic tracked_files
26
26
  attr_reader :tracked_files #: Array[String]
@@ -36,6 +36,7 @@ class Riffer::Rig::Loader
36
36
  # @rbs agents_md: bool
37
37
  # @rbs tools: Array[String]?
38
38
  # @rbs max_steps: Integer?
39
+ # @rbs store: Riffer::Rig::Stores::_Store | nil
39
40
  # @rbs return: Riffer::Rig::Runtime
40
41
  def self.runtime(
41
42
  cwd:,
@@ -48,9 +49,11 @@ class Riffer::Rig::Loader
48
49
  skills: true,
49
50
  agents_md: true,
50
51
  tools: nil,
51
- max_steps: Riffer::Rig::Runtime::DEFAULT_MAX_STEPS
52
+ max_steps: Riffer::Rig::Runtime::DEFAULT_MAX_STEPS,
53
+ store: Riffer::Rig::Stores::JSONL.new
52
54
  )
53
- new(cwd:, host:, env:, home:, riffer_config:).runtime(model:, extensions:, skills:, agents_md:, tools:, max_steps:)
55
+ new(cwd:, host:, env:, home:, riffer_config:, store:)
56
+ .runtime(model:, extensions:, skills:, agents_md:, tools:, max_steps:)
54
57
  end
55
58
 
56
59
  # @rbs cwd: String
@@ -58,8 +61,16 @@ class Riffer::Rig::Loader
58
61
  # @rbs env: Riffer::Rig::Env | Riffer::Rig::Env::Invalid
59
62
  # @rbs home: String
60
63
  # @rbs riffer_config: Riffer::Config
64
+ # @rbs store: Riffer::Rig::Stores::_Store | nil
61
65
  # @rbs return: void
62
- def initialize(cwd:, host:, env: Riffer::Rig::Env.load, home: Dir.home, riffer_config: Riffer.config)
66
+ def initialize(
67
+ cwd:,
68
+ host:,
69
+ store: Riffer::Rig::Stores::JSONL.new,
70
+ env: Riffer::Rig::Env.load,
71
+ home: Dir.home,
72
+ riffer_config: Riffer.config
73
+ )
63
74
  raise ConfigurationError, env.message if env.is_a?(Riffer::Rig::Env::Invalid)
64
75
 
65
76
  @cwd = cwd
@@ -67,7 +78,12 @@ class Riffer::Rig::Loader
67
78
  @env = env
68
79
  @home = home
69
80
  @riffer_config = riffer_config
81
+ @store = store
70
82
  @tracked_files = []
83
+ @stripped = []
84
+ @extensions_enabled = true
85
+ @gem_extensions = []
86
+ @file_state = nil
71
87
  end
72
88
 
73
89
  # @rbs model: String?
@@ -85,19 +101,23 @@ class Riffer::Rig::Loader
85
101
  tools: nil,
86
102
  max_steps: Riffer::Rig::Runtime::DEFAULT_MAX_STEPS
87
103
  )
88
- settings = Riffer::Rig::Settings.merge(
89
- Riffer::Rig::Settings.read(home_settings_path),
90
- Riffer::Rig::Settings.read(File.join(@cwd, '.riffer', 'settings.json'))
91
- )
104
+ settings = merged_settings
92
105
  document = Riffer::Rig::Settings::Document.new(settings)
93
106
  selected = select_model(model || @env.model || document.model)
94
107
  provider = Riffer::Rig::Settings.provider_for(selected).to_s
108
+ ensure_sdk(provider)
95
109
  credentials = { provider.to_sym => credentials_for(provider) }
96
- stripped = { skills: skills, agents_md: agents_md }.reject { |_name, kept| kept }.keys
97
- loaded = Riffer::Rig::Bundled::BY_NAME.except(*document.disabled.map(&:to_sym), *stripped).values
110
+ @extensions_enabled = extensions
111
+ @stripped = { skills: skills, agents_md: agents_md }.reject { |_name, kept| kept }.keys
112
+ loaded = Riffer::Rig::Bundled::BY_NAME.except(*document.disabled.map(&:to_sym), *@stripped).values
113
+ @tracked_files = []
114
+ @gem_extensions = []
98
115
  loaded += load_rig_files(document.autoload) if extensions
99
116
 
100
- Riffer::Rig::Runtime.new(
117
+ recorder = recorder_for(document)
118
+ loaded << Riffer::Rig::Stores::Recorder.extension(recorder) if recorder
119
+
120
+ built = Riffer::Rig::Runtime.new(
101
121
  selected,
102
122
  extensions: loaded,
103
123
  tools: tools,
@@ -108,20 +128,180 @@ class Riffer::Rig::Loader
108
128
  credentials: credentials,
109
129
  pricing: document.models,
110
130
  riffer_config: @riffer_config,
111
- model_options: Riffer::Rig::Settings.model_options(selected, document.reasoning)
131
+ model_options: Riffer::Rig::Settings.model_options(selected, document.reasoning),
132
+ native_tools: document.native_tools
112
133
  )
134
+ built.install_command(Riffer::Rig::Commands::Reload.command(self))
135
+ @file_state = file_state
136
+ recorder&.attach(built)
137
+ built
138
+ end
139
+
140
+ # Without force, a no-op while the tracked file set and the two rig.rb paths
141
+ # look as the last discovery left them — the entry point the automatic
142
+ # trigger (#133) calls. The snapshot advances on every attempt, failed ones
143
+ # included, so a failure does not retry every request. Failures are notified
144
+ # once and reported as the message, the way Settings.rejection reports a
145
+ # rejection; the Runtime is the success value.
146
+ # @rbs runtime: Riffer::Rig::Runtime
147
+ # @rbs force: bool
148
+ # @rbs return: (Riffer::Rig::Runtime | String)
149
+ def reload(runtime, force: false)
150
+ return runtime if !force && file_state == @file_state
151
+
152
+ outcome = perform_reload(runtime)
153
+ @file_state = file_state
154
+ outcome
113
155
  end
114
156
 
115
157
  private
116
158
 
159
+ # @rbs return: Hash[Symbol, untyped]
160
+ def merged_settings
161
+ Riffer::Rig::Settings.merge(
162
+ Riffer::Rig::Settings.read(home_settings_path),
163
+ Riffer::Rig::Settings.read(File.join(@cwd, '.riffer', 'settings.json'))
164
+ )
165
+ end
166
+
167
+ # Notifies every failure once and reports its message; a rebuilt Runtime is
168
+ # the success value.
169
+ # @rbs runtime: Riffer::Rig::Runtime
170
+ # @rbs return: (Riffer::Rig::Runtime | String)
171
+ def perform_reload(runtime)
172
+ settings = merged_settings
173
+ document = Riffer::Rig::Settings::Document.new(settings)
174
+ begin
175
+ extensions = reload_extensions(document, settings)
176
+ rescue AbandonedError => e
177
+ return notify_failure(e.message)
178
+ end
179
+
180
+ failure = reload_credentials(runtime)
181
+ return notify_failure(failure) if failure
182
+
183
+ begin
184
+ runtime.rebuild(extensions: validated(extensions, settings), settings: settings)
185
+ rescue StandardError => e
186
+ return notify_failure(e.message)
187
+ end
188
+ runtime
189
+ end
190
+
191
+ # @rbs message: String
192
+ # @rbs return: String
193
+ def notify_failure(message)
194
+ @host.notify(message, level: :error)
195
+ message
196
+ end
197
+
198
+ # @rbs document: Riffer::Rig::Settings::Document
199
+ # @rbs settings: Hash[Symbol, untyped]
200
+ # @rbs return: Array[Riffer::Rig::Extension]
201
+ def reload_extensions(document, settings)
202
+ candidates = Riffer::Rig::Bundled::BY_NAME.except(*document.disabled.map(&:to_sym), *@stripped).values
203
+ return validated(candidates, settings) unless @extensions_enabled
204
+
205
+ scrub_tracked_features
206
+ @tracked_files = []
207
+ candidates.concat(@gem_extensions, rig_recordings)
208
+ validated(candidates, settings)
209
+ end
210
+
211
+ # @rbs return: Array[Riffer::Rig::Extension]
212
+ def rig_recordings
213
+ recorded = [] #: Array[Riffer::Rig::Extension]
214
+ recorded.concat(load_rig_file(home_rig_path, confirm_trust: false))
215
+ recorded.concat(load_rig_file(project_rig_path, confirm_trust: true))
216
+ recorded
217
+ end
218
+
219
+ # @rbs path: String
220
+ # @rbs confirm_trust: bool
221
+ # @rbs return: Array[Riffer::Rig::Extension]
222
+ def load_rig_file(path, confirm_trust:)
223
+ return [] unless File.file?(path)
224
+ return [] if confirm_trust && !trusted?(path)
225
+
226
+ load_file(path, track: true, strict: true)
227
+ end
228
+
229
+ # @rbs return: void
230
+ def scrub_tracked_features
231
+ @tracked_files.each { |path| $LOADED_FEATURES.delete(path) }
232
+ end
233
+
234
+ # Runs each extension against a throwaway registrar so that a failing block
235
+ # is skipped and reported instead of aborting the rebuild.
236
+ # @rbs extensions: Array[Riffer::Rig::Extension]
237
+ # @rbs settings: Hash[Symbol, untyped]
238
+ # @rbs return: Array[Riffer::Rig::Extension]
239
+ def validated(extensions, settings)
240
+ extensions.select do |extension|
241
+ registrar = Riffer::Rig::Registrar.new(extension.name, settings[extension.name.to_sym] || {})
242
+ error = extension.load_into(registrar)
243
+ next true unless error
244
+
245
+ @host.notify("Extension #{extension.name} failed to load: #{error.message}", level: :error)
246
+ false
247
+ end
248
+ end
249
+
250
+ # @rbs runtime: Riffer::Rig::Runtime
251
+ # @rbs return: String?
252
+ def reload_credentials(runtime)
253
+ provider = runtime.model.partition('/').first
254
+ resolution = Riffer::Rig::Credentials.resolve(
255
+ provider,
256
+ host: asking_host,
257
+ env: @env,
258
+ auth_path: File.join(@home, '.riffer', 'auth.json'),
259
+ settings_path: home_settings_path
260
+ )
261
+ return missing_credentials(provider, resolution.missing) unless resolution.missing.empty?
262
+
263
+ Riffer::Rig::Credentials.apply(provider, resolution.values, config: @riffer_config)
264
+ runtime.merge_credentials(provider.to_sym, resolution.values)
265
+ nil
266
+ end
267
+
268
+ # @rbs return: Hash[String, Time?]
269
+ def file_state
270
+ (@tracked_files + rig_paths).uniq.to_h { |path| [path, stamp(path)] }
271
+ end
272
+
273
+ # @rbs return: Array[String]
274
+ def rig_paths
275
+ return [] unless @extensions_enabled
276
+
277
+ [home_rig_path, project_rig_path]
278
+ end
279
+
280
+ # @rbs path: String
281
+ # @rbs return: Time?
282
+ def stamp(path)
283
+ File.file?(path) ? File.mtime(path) : nil
284
+ end
285
+
286
+ # @rbs return: String
287
+ def home_rig_path
288
+ File.expand_path(File.join(@home, '.riffer', 'rig.rb'))
289
+ end
290
+
291
+ # @rbs return: String
292
+ def project_rig_path
293
+ File.expand_path(File.join(@cwd, '.riffer', 'rig.rb'))
294
+ end
295
+
117
296
  # @rbs autoload: bool
118
297
  # @rbs return: Array[Riffer::Rig::Extension]
119
298
  def load_rig_files(autoload)
120
- recorded = gem_extension_files(autoload).flat_map { |file| load_file(File.expand_path(file), track: false) }
121
- home = File.expand_path(File.join(@home, '.riffer', 'rig.rb'))
122
- recorded.concat(load_file(home, track: true)) if File.file?(home)
123
- project = File.expand_path(File.join(@cwd, '.riffer', 'rig.rb'))
124
- recorded.concat(load_file(project, track: true)) if File.file?(project) && trusted?(project)
299
+ @gem_extensions = gem_extension_files(autoload).flat_map { |file| load_file(File.expand_path(file), track: false) }
300
+ recorded = @gem_extensions.dup
301
+ recorded.concat(load_file(home_rig_path, track: true)) if File.file?(home_rig_path)
302
+ if File.file?(project_rig_path) && trusted?(project_rig_path)
303
+ recorded.concat(load_file(project_rig_path, track: true))
304
+ end
125
305
  recorded
126
306
  end
127
307
 
@@ -150,17 +330,25 @@ class Riffer::Rig::Loader
150
330
  File.join(@home, '.riffer', 'trust.json')
151
331
  end
152
332
 
333
+ # Loads one file and returns the extensions it recorded. A tracked load adds
334
+ # the features the load added to the tracked file set. A failure is reported
335
+ # to the host and yields nothing, unless strict: then it propagates as
336
+ # AbandonedError for the reload to abandon on.
153
337
  # @rbs path: String
154
338
  # @rbs track: bool
339
+ # @rbs strict: bool
155
340
  # @rbs return: Array[Riffer::Rig::Extension]
156
- def load_file(path, track:)
341
+ def load_file(path, track:, strict: false)
157
342
  before = Riffer::Rig.extensions
158
343
  before_features = $LOADED_FEATURES.dup
159
344
  begin
160
345
  load path
161
346
  rescue StandardError => e
162
347
  track_loaded(before_features, path) if track
163
- @host.notify("#{path} failed to load: #{e.message}", level: :error)
348
+ message = "#{path} failed to load: #{e.message}"
349
+ raise AbandonedError, message if strict
350
+
351
+ @host.notify(message, level: :error)
164
352
  return []
165
353
  end
166
354
  track_loaded(before_features, path) if track
@@ -195,12 +383,40 @@ class Riffer::Rig::Loader
195
383
 
196
384
  # @rbs return: String
197
385
  def ask_for_model
198
- answer = asking_host.ask(ONBOARDING_QUESTION).to_s.strip
199
- raise ConfigurationError, NO_MODEL if answer.empty?
386
+ answer = asking_host.ask(onboarding_question).to_s.strip
387
+ raise ConfigurationError, no_model if answer.empty?
200
388
 
201
389
  answer
202
390
  end
203
391
 
392
+ # @rbs return: String
393
+ def onboarding_question
394
+ 'Which model should riffer use? Enter provider/name, ' \
395
+ "with a provider from: #{provider_list}"
396
+ end
397
+
398
+ # @rbs return: String
399
+ def no_model
400
+ 'No model is set: pass --model provider/name, set RIFFER_MODEL, or set "model" in ' \
401
+ "~/.riffer/settings.json, with a provider from: #{provider_list}"
402
+ end
403
+
404
+ # @rbs return: String
405
+ def provider_list
406
+ Riffer::Rig::Settings.providers.join(', ')
407
+ end
408
+
409
+ # @rbs provider: String
410
+ # @rbs return: void
411
+ def ensure_sdk(provider)
412
+ sdk = Riffer::Rig::ProviderSetup.for(provider).sdk
413
+ return unless sdk
414
+
415
+ gem, requirement = sdk
416
+ message = Riffer::Rig::SDK.ensure(gem, requirement, host: asking_host)
417
+ raise ConfigurationError, message if message
418
+ end
419
+
204
420
  # @rbs provider: String
205
421
  # @rbs return: Hash[Symbol, String]
206
422
  def credentials_for(provider)
@@ -222,9 +438,8 @@ class Riffer::Rig::Loader
222
438
  # @rbs return: String
223
439
  def missing_credentials(provider, missing)
224
440
  setup = Riffer::Rig::ProviderSetup.for(provider)
225
- fields = setup.fields.select { |field| missing.include?(field.name) }
226
- wanted = fields.map { |field| "#{field.name} (#{field.env.join(' or ')})" }.join(', ')
227
- "#{provider} has no #{wanted}; set it in the environment or run riffer interactively to paste it" \
441
+ "#{provider} has no #{setup.missing_fields(missing)}; " \
442
+ 'set it in the environment or run riffer interactively to paste it' \
228
443
  "#{" (create one at #{setup.url})" if setup.url}"
229
444
  end
230
445
 
@@ -237,4 +452,16 @@ class Riffer::Rig::Loader
237
452
  def home_settings_path
238
453
  File.join(@home, '.riffer', 'settings.json')
239
454
  end
455
+
456
+ # The store records unless it was declined with store: nil or globally with
457
+ # "sessions": {"save": false}.
458
+ # @rbs document: Riffer::Rig::Settings::Document
459
+ # @rbs return: Riffer::Rig::Stores::Recorder?
460
+ def recorder_for(document)
461
+ store = @store
462
+ return nil unless document.save
463
+ return nil unless store
464
+
465
+ Riffer::Rig::Stores::Recorder.new(store: store)
466
+ end
240
467
  end
@@ -1,18 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Riffer::Rig::ProviderSetup
4
- # @dynamic url, chain, fields
4
+ # @dynamic url, chain, sdk, fields
5
5
  attr_reader :url #: String?
6
6
  attr_reader :chain #: bool
7
+ attr_reader :sdk #: [String, String]?
7
8
  attr_reader :fields #: Array[Riffer::Rig::ProviderSetup::Field]
8
9
 
9
10
  # @rbs fields: Array[Riffer::Rig::ProviderSetup::Field]
10
11
  # @rbs url: String?
11
12
  # @rbs chain: bool
13
+ # @rbs sdk: [String, String]?
12
14
  # @rbs return: void
13
- def initialize(fields:, url: nil, chain: false)
15
+ def initialize(fields:, url: nil, chain: false, sdk: nil)
14
16
  @url = url
15
17
  @chain = chain
18
+ @sdk = sdk&.freeze
16
19
  @fields = fields.freeze
17
20
  freeze
18
21
  end
@@ -24,15 +27,17 @@ class Riffer::Rig::ProviderSetup
24
27
  nil
25
28
  end #: ^() -> String?
26
29
 
27
- # Upstream candidate: each riffer provider could declare its own credential
28
- # fields, and this table would go.
30
+ # Upstream candidate: riffer's providers could declare their own SDK gem and
31
+ # credential fields, and this table would go.
29
32
  TABLE = {
30
33
  anthropic: new(
31
34
  url: 'https://console.anthropic.com/settings/keys',
35
+ sdk: ['anthropic', '~> 1.69'],
32
36
  fields: [Field.new(name: :api_key, env: ['ANTHROPIC_API_KEY'], secret: true, required: true)]
33
37
  ),
34
38
  openai: new(
35
39
  url: 'https://platform.openai.com/api-keys',
40
+ sdk: ['openai', '~> 0.80'],
36
41
  fields: [
37
42
  Field.new(name: :api_key, env: ['OPENAI_API_KEY'], secret: true, required: true),
38
43
  Field.new(name: :base_url, env: ['OPENAI_BASE_URL'], secret: false, required: false)
@@ -44,10 +49,12 @@ class Riffer::Rig::ProviderSetup
44
49
  ),
45
50
  openrouter: new(
46
51
  url: 'https://openrouter.ai/keys',
52
+ sdk: ['openai', '~> 0.80'],
47
53
  fields: [Field.new(name: :api_key, env: ['OPENROUTER_API_KEY'], secret: true, required: true)]
48
54
  ),
49
55
  azure_openai: new(
50
56
  url: 'https://portal.azure.com',
57
+ sdk: ['openai', '~> 0.80'],
51
58
  fields: [
52
59
  Field.new(name: :endpoint, env: ['AZURE_OPENAI_ENDPOINT'], secret: false, required: true),
53
60
  Field.new(name: :api_key, env: ['AZURE_OPENAI_API_KEY'], secret: true, required: true)
@@ -56,6 +63,7 @@ class Riffer::Rig::ProviderSetup
56
63
  amazon_bedrock: new(
57
64
  url: 'https://console.aws.amazon.com/bedrock',
58
65
  chain: true,
66
+ sdk: ['aws-sdk-bedrockruntime', '~> 1.0'],
59
67
  fields: [
60
68
  Field.new(
61
69
  name: :region,
@@ -69,10 +77,33 @@ class Riffer::Rig::ProviderSetup
69
77
  )
70
78
  }.freeze #: Hash[Symbol, Riffer::Rig::ProviderSetup]
71
79
 
80
+ # @rbs missing: Array[Symbol]
81
+ # @rbs return: String
82
+ def missing_fields(missing)
83
+ fields.select { |field| missing.include?(field.name) }
84
+ .map { |field| "#{field.name} (#{field.env.join(' or ')})" }
85
+ .join(', ')
86
+ end
87
+
88
+ # @rbs hash: Hash[Symbol, untyped]
89
+ # @rbs return: Riffer::Rig::ProviderSetup
90
+ def self.from(hash)
91
+ fields = hash.fetch(:fields).map do |field|
92
+ Field.new(
93
+ name: field.fetch(:name),
94
+ env: field.fetch(:env),
95
+ secret: field.fetch(:secret, false),
96
+ required: field.fetch(:required, false),
97
+ fallback: field[:fallback]
98
+ )
99
+ end
100
+ new(fields: fields, url: hash[:url], chain: hash.fetch(:chain, false), sdk: hash[:sdk])
101
+ end
102
+
72
103
  # @rbs identifier: String | Symbol
73
104
  # @rbs return: Riffer::Rig::ProviderSetup?
74
105
  def self.[](identifier)
75
- TABLE[identifier.to_sym]
106
+ Riffer::Rig::Providers.setup(identifier) || TABLE[identifier.to_sym]
76
107
  end
77
108
 
78
109
  # @rbs identifier: String | Symbol
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Riffer::Rig::Providers
4
+ extend self
5
+
6
+ # Upstream candidate: riffer's Repository exposes no listing of extension
7
+ # registrations and stores no credential setups, so this registry mirrors
8
+ # what the provider seam registers.
9
+ # @rbs @setups: Hash[Symbol, Riffer::Rig::ProviderSetup?]
10
+ @setups = {} #: Hash[Symbol, Riffer::Rig::ProviderSetup?]
11
+
12
+ # @rbs identifier: String | Symbol
13
+ # @rbs setup: Riffer::Rig::ProviderSetup | Hash[Symbol, untyped]?
14
+ # @rbs &block: () -> singleton(::Riffer::Providers::Base)
15
+ # @rbs return: void
16
+ def register(identifier, setup: nil, &)
17
+ ::Riffer::Providers::Repository.register(identifier, &)
18
+ @setups[identifier.to_sym] = setup && normalize(setup)
19
+ nil
20
+ end
21
+
22
+ # @rbs identifier: String | Symbol
23
+ # @rbs return: void
24
+ def unregister(identifier)
25
+ ::Riffer::Providers::Repository.unregister(identifier)
26
+ @setups.delete(identifier.to_sym)
27
+ nil
28
+ end
29
+
30
+ # @rbs identifier: String | Symbol
31
+ # @rbs return: Riffer::Rig::ProviderSetup?
32
+ def setup(identifier)
33
+ @setups[identifier.to_sym]
34
+ end
35
+
36
+ # @rbs return: Array[Symbol]
37
+ def identifiers
38
+ (::Riffer::Providers::Repository::REPO.keys - [:mock]) | @setups.keys
39
+ end
40
+
41
+ private
42
+
43
+ # @rbs setup: Riffer::Rig::ProviderSetup | Hash[Symbol, untyped]
44
+ # @rbs return: Riffer::Rig::ProviderSetup
45
+ def normalize(setup)
46
+ setup.is_a?(::Riffer::Rig::ProviderSetup) ? setup : ::Riffer::Rig::ProviderSetup.from(setup)
47
+ end
48
+ end
@@ -88,6 +88,14 @@ class Riffer::Rig::Registrar
88
88
  @mcp_servers[name] = Riffer::Rig::Mcp::Declaration.new(url: url, headers: headers)
89
89
  end
90
90
 
91
+ # @rbs prefix: String | Symbol
92
+ # @rbs setup: Riffer::Rig::ProviderSetup | Hash[Symbol, untyped]?
93
+ # @rbs &block: () -> singleton(::Riffer::Providers::Base)
94
+ # @rbs return: void
95
+ def provider(prefix, setup: nil, &)
96
+ Riffer::Rig::Providers.register(prefix, setup:, &)
97
+ end
98
+
91
99
  # @rbs event: Symbol
92
100
  # @rbs &block: (Riffer::Rig::Events::_Event | ::Riffer::StreamEvents::Base) -> untyped
93
101
  # @rbs return: void