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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +11 -2
- data/lib/riffer/rig/cli/flags.rb +16 -2
- data/lib/riffer/rig/cli.rb +2 -1
- data/lib/riffer/rig/commands/auth.rb +79 -0
- data/lib/riffer/rig/commands/model.rb +70 -21
- data/lib/riffer/rig/commands/reload.rb +23 -0
- data/lib/riffer/rig/credentials.rb +13 -1
- data/lib/riffer/rig/extension.rb +12 -0
- data/lib/riffer/rig/loader.rb +258 -31
- data/lib/riffer/rig/provider_setup.rb +36 -5
- data/lib/riffer/rig/providers.rb +48 -0
- data/lib/riffer/rig/registrar.rb +8 -0
- data/lib/riffer/rig/runtime.rb +110 -21
- data/lib/riffer/rig/sdk.rb +62 -0
- data/lib/riffer/rig/settings/document.rb +7 -1
- data/lib/riffer/rig/settings.rb +5 -4
- data/lib/riffer/rig/stores/jsonl.rb +115 -0
- data/lib/riffer/rig/stores/recorder.rb +119 -0
- data/lib/riffer/rig/version.rb +1 -1
- data/lib/riffer/rig.rb +7 -1
- data/sig/generated/riffer/rig/cli/flags.rbs +5 -2
- data/sig/generated/riffer/rig/commands/auth.rbs +35 -0
- data/sig/generated/riffer/rig/commands/model.rbs +21 -9
- data/sig/generated/riffer/rig/commands/reload.rbs +12 -0
- data/sig/generated/riffer/rig/credentials.rbs +8 -0
- data/sig/generated/riffer/rig/extension.rbs +4 -0
- data/sig/generated/riffer/rig/loader.rbs +108 -7
- data/sig/generated/riffer/rig/provider_setup.rbs +15 -4
- data/sig/generated/riffer/rig/providers.rbs +28 -0
- data/sig/generated/riffer/rig/registrar.rbs +6 -0
- data/sig/generated/riffer/rig/runtime.rbs +63 -9
- data/sig/generated/riffer/rig/sdk.rbs +33 -0
- data/sig/generated/riffer/rig/settings/document.rbs +5 -1
- data/sig/generated/riffer/rig/settings.rbs +2 -3
- data/sig/generated/riffer/rig/stores/jsonl.rbs +64 -0
- data/sig/generated/riffer/rig/stores/recorder.rbs +67 -0
- data/sig/generated/riffer/rig.rbs +4 -0
- data/sig/manual/riffer/rig/commands/auth.rbs +6 -0
- data/sig/manual/riffer/rig/commands/reload.rbs +6 -0
- data/sig/manual/riffer/rig/providers.rbs +6 -0
- data/sig/manual/riffer/rig/sdk.rbs +6 -0
- data/sig/manual/riffer/rig/stores.rbs +11 -0
- metadata +18 -29
data/lib/riffer/rig/loader.rb
CHANGED
|
@@ -3,24 +3,24 @@
|
|
|
3
3
|
class Riffer::Rig::Loader
|
|
4
4
|
class ConfigurationError < StandardError; end
|
|
5
5
|
|
|
6
|
-
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
recorded.concat(load_file(
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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(
|
|
199
|
-
raise ConfigurationError,
|
|
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
|
-
|
|
226
|
-
|
|
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:
|
|
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
|
data/lib/riffer/rig/registrar.rb
CHANGED
|
@@ -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
|