riffer-rig 0.7.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 +30 -0
- data/README.md +13 -3
- 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 +327 -23
- 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 +9 -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/terminal/renderer.rb +86 -13
- data/lib/riffer/rig/terminal/wrapper.rb +103 -0
- data/lib/riffer/rig/trust.rb +37 -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 +142 -5
- 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 +7 -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/terminal/renderer.rbs +41 -1
- data/sig/generated/riffer/rig/terminal/wrapper.rbs +43 -0
- data/sig/generated/riffer/rig/trust.rbs +20 -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
- data/sig/manual/riffer/rig/trust.rbs +6 -0
- metadata +23 -29
data/lib/riffer/rig/loader.rb
CHANGED
|
@@ -3,21 +3,27 @@
|
|
|
3
3
|
class Riffer::Rig::Loader
|
|
4
4
|
class ConfigurationError < StandardError; end
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
# Raised by a strict load: the reload abandons instead of continuing.
|
|
7
|
+
class AbandonedError < StandardError; end
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
"with a provider from: #{PROVIDER_LIST}".freeze #: String
|
|
9
|
+
EXTENSION_FEATURE = 'riffer/rig/extension' #: String
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
"~/.riffer/settings.json, with a provider from: #{PROVIDER_LIST}".freeze #: String
|
|
13
|
-
|
|
14
|
-
private_constant :PROVIDER_LIST, :ONBOARDING_QUESTION, :NO_MODEL
|
|
11
|
+
private_constant :EXTENSION_FEATURE
|
|
15
12
|
|
|
16
13
|
# @rbs @cwd: String
|
|
17
14
|
# @rbs @host: Riffer::Rig::Hosts::_Host
|
|
18
15
|
# @rbs @env: Riffer::Rig::Env
|
|
19
16
|
# @rbs @home: String
|
|
20
17
|
# @rbs @riffer_config: Riffer::Config
|
|
18
|
+
# @rbs @store: Riffer::Rig::Stores::_Store | nil
|
|
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
|
+
|
|
25
|
+
# @dynamic tracked_files
|
|
26
|
+
attr_reader :tracked_files #: Array[String]
|
|
21
27
|
|
|
22
28
|
# @rbs cwd: String
|
|
23
29
|
# @rbs host: Riffer::Rig::Hosts::_Host
|
|
@@ -30,6 +36,7 @@ class Riffer::Rig::Loader
|
|
|
30
36
|
# @rbs agents_md: bool
|
|
31
37
|
# @rbs tools: Array[String]?
|
|
32
38
|
# @rbs max_steps: Integer?
|
|
39
|
+
# @rbs store: Riffer::Rig::Stores::_Store | nil
|
|
33
40
|
# @rbs return: Riffer::Rig::Runtime
|
|
34
41
|
def self.runtime(
|
|
35
42
|
cwd:,
|
|
@@ -42,9 +49,11 @@ class Riffer::Rig::Loader
|
|
|
42
49
|
skills: true,
|
|
43
50
|
agents_md: true,
|
|
44
51
|
tools: nil,
|
|
45
|
-
max_steps: Riffer::Rig::Runtime::DEFAULT_MAX_STEPS
|
|
52
|
+
max_steps: Riffer::Rig::Runtime::DEFAULT_MAX_STEPS,
|
|
53
|
+
store: Riffer::Rig::Stores::JSONL.new
|
|
46
54
|
)
|
|
47
|
-
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:)
|
|
48
57
|
end
|
|
49
58
|
|
|
50
59
|
# @rbs cwd: String
|
|
@@ -52,8 +61,16 @@ class Riffer::Rig::Loader
|
|
|
52
61
|
# @rbs env: Riffer::Rig::Env | Riffer::Rig::Env::Invalid
|
|
53
62
|
# @rbs home: String
|
|
54
63
|
# @rbs riffer_config: Riffer::Config
|
|
64
|
+
# @rbs store: Riffer::Rig::Stores::_Store | nil
|
|
55
65
|
# @rbs return: void
|
|
56
|
-
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
|
+
)
|
|
57
74
|
raise ConfigurationError, env.message if env.is_a?(Riffer::Rig::Env::Invalid)
|
|
58
75
|
|
|
59
76
|
@cwd = cwd
|
|
@@ -61,6 +78,12 @@ class Riffer::Rig::Loader
|
|
|
61
78
|
@env = env
|
|
62
79
|
@home = home
|
|
63
80
|
@riffer_config = riffer_config
|
|
81
|
+
@store = store
|
|
82
|
+
@tracked_files = []
|
|
83
|
+
@stripped = []
|
|
84
|
+
@extensions_enabled = true
|
|
85
|
+
@gem_extensions = []
|
|
86
|
+
@file_state = nil
|
|
64
87
|
end
|
|
65
88
|
|
|
66
89
|
# @rbs model: String?
|
|
@@ -78,19 +101,25 @@ class Riffer::Rig::Loader
|
|
|
78
101
|
tools: nil,
|
|
79
102
|
max_steps: Riffer::Rig::Runtime::DEFAULT_MAX_STEPS
|
|
80
103
|
)
|
|
81
|
-
settings =
|
|
82
|
-
Riffer::Rig::Settings.read(home_settings_path),
|
|
83
|
-
Riffer::Rig::Settings.read(File.join(@cwd, '.riffer', 'settings.json'))
|
|
84
|
-
)
|
|
104
|
+
settings = merged_settings
|
|
85
105
|
document = Riffer::Rig::Settings::Document.new(settings)
|
|
86
106
|
selected = select_model(model || @env.model || document.model)
|
|
87
107
|
provider = Riffer::Rig::Settings.provider_for(selected).to_s
|
|
108
|
+
ensure_sdk(provider)
|
|
88
109
|
credentials = { provider.to_sym => credentials_for(provider) }
|
|
89
|
-
|
|
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 = []
|
|
115
|
+
loaded += load_rig_files(document.autoload) if extensions
|
|
116
|
+
|
|
117
|
+
recorder = recorder_for(document)
|
|
118
|
+
loaded << Riffer::Rig::Stores::Recorder.extension(recorder) if recorder
|
|
90
119
|
|
|
91
|
-
Riffer::Rig::Runtime.new(
|
|
120
|
+
built = Riffer::Rig::Runtime.new(
|
|
92
121
|
selected,
|
|
93
|
-
extensions:
|
|
122
|
+
extensions: loaded,
|
|
94
123
|
tools: tools,
|
|
95
124
|
settings: settings,
|
|
96
125
|
host: @host,
|
|
@@ -99,12 +128,248 @@ class Riffer::Rig::Loader
|
|
|
99
128
|
credentials: credentials,
|
|
100
129
|
pricing: document.models,
|
|
101
130
|
riffer_config: @riffer_config,
|
|
102
|
-
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
|
|
103
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
|
|
104
155
|
end
|
|
105
156
|
|
|
106
157
|
private
|
|
107
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
|
+
|
|
296
|
+
# @rbs autoload: bool
|
|
297
|
+
# @rbs return: Array[Riffer::Rig::Extension]
|
|
298
|
+
def load_rig_files(autoload)
|
|
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
|
|
305
|
+
recorded
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# @rbs autoload: bool
|
|
309
|
+
# @rbs return: Array[String]
|
|
310
|
+
def gem_extension_files(autoload)
|
|
311
|
+
return [] unless autoload
|
|
312
|
+
|
|
313
|
+
Gem.find_files(EXTENSION_FEATURE)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# @rbs path: String
|
|
317
|
+
# @rbs return: bool
|
|
318
|
+
def trusted?(path)
|
|
319
|
+
stored = Riffer::Rig::Trust.read(trust_path)[path]
|
|
320
|
+
return stored unless stored.nil?
|
|
321
|
+
|
|
322
|
+
asked = @host.capabilities.include?(:confirm)
|
|
323
|
+
decision = asked ? @host.confirm("Trust #{path}?") : false
|
|
324
|
+
Riffer::Rig::Trust.store(trust_path, path, decision) if asked
|
|
325
|
+
decision
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# @rbs return: String
|
|
329
|
+
def trust_path
|
|
330
|
+
File.join(@home, '.riffer', 'trust.json')
|
|
331
|
+
end
|
|
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.
|
|
337
|
+
# @rbs path: String
|
|
338
|
+
# @rbs track: bool
|
|
339
|
+
# @rbs strict: bool
|
|
340
|
+
# @rbs return: Array[Riffer::Rig::Extension]
|
|
341
|
+
def load_file(path, track:, strict: false)
|
|
342
|
+
before = Riffer::Rig.extensions
|
|
343
|
+
before_features = $LOADED_FEATURES.dup
|
|
344
|
+
begin
|
|
345
|
+
load path
|
|
346
|
+
rescue StandardError => e
|
|
347
|
+
track_loaded(before_features, path) if track
|
|
348
|
+
message = "#{path} failed to load: #{e.message}"
|
|
349
|
+
raise AbandonedError, message if strict
|
|
350
|
+
|
|
351
|
+
@host.notify(message, level: :error)
|
|
352
|
+
return []
|
|
353
|
+
end
|
|
354
|
+
track_loaded(before_features, path) if track
|
|
355
|
+
Riffer::Rig.extensions.values.reject { |extension| before[extension.name] == extension }
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# @rbs before_features: Array[String]
|
|
359
|
+
# @rbs path: String
|
|
360
|
+
# @rbs return: void
|
|
361
|
+
def track_loaded(before_features, path)
|
|
362
|
+
recorded = ($LOADED_FEATURES - before_features) + [path] #: Array[String]
|
|
363
|
+
@tracked_files.concat(recorded.select { |feature| local?(feature) })
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# @rbs feature: String
|
|
367
|
+
# @rbs return: bool
|
|
368
|
+
def local?(feature)
|
|
369
|
+
roots = [File.expand_path(File.join(@home, '.riffer')), File.expand_path(@cwd)] #: Array[String]
|
|
370
|
+
roots.any? { |root| feature.start_with?("#{root}#{File::SEPARATOR}") }
|
|
371
|
+
end
|
|
372
|
+
|
|
108
373
|
# @rbs configured: String?
|
|
109
374
|
# @rbs return: String
|
|
110
375
|
def select_model(configured)
|
|
@@ -118,12 +383,40 @@ class Riffer::Rig::Loader
|
|
|
118
383
|
|
|
119
384
|
# @rbs return: String
|
|
120
385
|
def ask_for_model
|
|
121
|
-
answer = asking_host.ask(
|
|
122
|
-
raise ConfigurationError,
|
|
386
|
+
answer = asking_host.ask(onboarding_question).to_s.strip
|
|
387
|
+
raise ConfigurationError, no_model if answer.empty?
|
|
123
388
|
|
|
124
389
|
answer
|
|
125
390
|
end
|
|
126
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
|
+
|
|
127
420
|
# @rbs provider: String
|
|
128
421
|
# @rbs return: Hash[Symbol, String]
|
|
129
422
|
def credentials_for(provider)
|
|
@@ -145,9 +438,8 @@ class Riffer::Rig::Loader
|
|
|
145
438
|
# @rbs return: String
|
|
146
439
|
def missing_credentials(provider, missing)
|
|
147
440
|
setup = Riffer::Rig::ProviderSetup.for(provider)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
"#{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' \
|
|
151
443
|
"#{" (create one at #{setup.url})" if setup.url}"
|
|
152
444
|
end
|
|
153
445
|
|
|
@@ -160,4 +452,16 @@ class Riffer::Rig::Loader
|
|
|
160
452
|
def home_settings_path
|
|
161
453
|
File.join(@home, '.riffer', 'settings.json')
|
|
162
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
|
|
163
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
|