Class: Legion::Extensions::Llm::Provider
- Inherits:
-
Object
- Object
- Legion::Extensions::Llm::Provider
show all
- Includes:
- Streaming, Logging::Helper
- Defined in:
- lib/legion/extensions/llm/provider.rb,
lib/legion/extensions/llm/provider/open_ai_compatible.rb
Overview
Base class for LLM providers.
Defined Under Namespace
Modules: OpenAICompatible
Instance Attribute Summary collapse
Class Method Summary
collapse
Instance Method Summary
collapse
-
#api_base ⇒ Object
-
#assume_models_exist? ⇒ Boolean
-
#cache_instance_key ⇒ Object
-
#cache_local_instance? ⇒ Boolean
── Cache helpers with local/shared tier selection ────────────────.
-
#capabilities ⇒ Object
-
#complete(messages, tools:, temperature:, model:, params: {}, headers: {}, schema: nil, thinking: nil, tool_prefs: nil) ⇒ Object
rubocop:disable Metrics/ParameterLists.
-
#config_base_url ⇒ Object
-
#configuration_requirements ⇒ Object
-
#configured? ⇒ Boolean
-
#embed(text, model:, dimensions:) ⇒ Object
-
#endpoint_manifest ⇒ Object
-
#find_reachable_url(urls) ⇒ Object
-
#format_messages(messages) ⇒ Object
-
#format_tool_calls(_tool_calls) ⇒ Object
-
#headers ⇒ Object
-
#initialize(config) ⇒ Provider
constructor
A new instance of Provider.
-
#list_models ⇒ Object
rubocop:enable Metrics/ParameterLists.
-
#local? ⇒ Boolean
-
#model_allowed?(model_name) ⇒ Boolean
-
#model_blacklist ⇒ Object
-
#model_cache_fetch(key, ttl:) ⇒ Object
-
#model_cache_get(key) ⇒ Object
-
#model_cache_set(key, value, ttl:) ⇒ Object
-
#model_whitelist ⇒ Object
── Model allow-list / deny-list filtering ────────────────────────.
-
#moderate(input, model:) ⇒ Object
-
#name ⇒ Object
-
#paint(prompt, model:, size:, with: nil, mask: nil, params: {}) ⇒ Object
rubocop:disable Metrics/ParameterLists.
-
#parse_error(response) ⇒ Object
-
#parse_tool_calls(_tool_calls) ⇒ Object
-
#readiness(live: false) ⇒ Object
-
#remote? ⇒ Boolean
-
#resolve_base_url ⇒ Object
── Multi-host base_url resolution ────────────────────────────────.
-
#slug ⇒ Object
-
#strip_scheme(url) ⇒ Object
-
#tls_enabled? ⇒ Boolean
-
#transcribe(audio_file, model:, language:) ⇒ Object
-
#url_reachable?(url) ⇒ Boolean
Methods included from Streaming
build_on_data_handler, build_stream_callback, build_stream_error_response, error_chunk?, faraday_1?, handle_data, handle_error_chunk, handle_error_event, handle_failed_response, handle_json_error_chunk, handle_parsed_error, handle_sse, handle_stream, json_error_payload?, parse_error_from_json, parse_streaming_error, process_stream_chunk, stream_response
Constructor Details
#initialize(config) ⇒ Provider
Returns a new instance of Provider.
13
14
15
16
17
|
# File 'lib/legion/extensions/llm/provider.rb', line 13
def initialize(config)
@config = config
ensure_configured!
@connection = Connection.new(self, @config)
end
|
Instance Attribute Details
#config ⇒ Object
Returns the value of attribute config.
11
12
13
|
# File 'lib/legion/extensions/llm/provider.rb', line 11
def config
@config
end
|
#connection ⇒ Object
Returns the value of attribute connection.
11
12
13
|
# File 'lib/legion/extensions/llm/provider.rb', line 11
def connection
@connection
end
|
Class Method Details
.assume_models_exist? ⇒ Boolean
322
323
324
|
# File 'lib/legion/extensions/llm/provider.rb', line 322
def assume_models_exist?
false
end
|
.capabilities ⇒ Object
302
303
304
|
# File 'lib/legion/extensions/llm/provider.rb', line 302
def capabilities
nil
end
|
.configuration_options ⇒ Object
310
311
312
|
# File 'lib/legion/extensions/llm/provider.rb', line 310
def configuration_options
[]
end
|
.configuration_requirements ⇒ Object
306
307
308
|
# File 'lib/legion/extensions/llm/provider.rb', line 306
def configuration_requirements
[]
end
|
330
331
332
|
# File 'lib/legion/extensions/llm/provider.rb', line 330
def configured?(config)
configuration_requirements.all? { |req| config.send(req) }
end
|
366
367
368
369
370
|
# File 'lib/legion/extensions/llm/provider.rb', line 366
def configured_providers(config)
providers.select do |_slug, provider_class|
provider_class.configured?(config)
end.values
end
|
372
373
374
375
376
|
# File 'lib/legion/extensions/llm/provider.rb', line 372
def configured_remote_providers(config)
providers.select do |_slug, provider_class|
provider_class.remote? && provider_class.configured?(config)
end.values
end
|
.for(model) ⇒ Object
Deprecated.
Use the extension registry instead. Will be removed in 1.0.
348
349
350
351
|
# File 'lib/legion/extensions/llm/provider.rb', line 348
def for(model)
model_info = Models.find(model)
resolve model_info.provider
end
|
.local? ⇒ Boolean
314
315
316
|
# File 'lib/legion/extensions/llm/provider.rb', line 314
def local?
false
end
|
.local_providers ⇒ Object
358
359
360
|
# File 'lib/legion/extensions/llm/provider.rb', line 358
def local_providers
providers.select { |_slug, provider_class| provider_class.local? }
end
|
.name ⇒ Object
294
295
296
|
# File 'lib/legion/extensions/llm/provider.rb', line 294
def name
to_s.split('::').last
end
|
.providers ⇒ Object
Deprecated.
Use the extension registry instead. Will be removed in 1.0.
354
355
356
|
# File 'lib/legion/extensions/llm/provider.rb', line 354
def providers
@providers ||= {}
end
|
.register(name, provider_class) ⇒ Object
Deprecated.
Use the extension registry instead. Will be removed in 1.0.
.remote? ⇒ Boolean
318
319
320
|
# File 'lib/legion/extensions/llm/provider.rb', line 318
def remote?
!local?
end
|
.remote_providers ⇒ Object
362
363
364
|
# File 'lib/legion/extensions/llm/provider.rb', line 362
def remote_providers
providers.select { |_slug, provider_class| provider_class.remote? }
end
|
.resolve(name) ⇒ Object
Deprecated.
Use the extension registry instead. Will be removed in 1.0.
341
342
343
344
345
|
# File 'lib/legion/extensions/llm/provider.rb', line 341
def resolve(name)
return nil if name.nil?
providers[name.to_sym]
end
|
.resolve_model_id(model_id, config: nil) ⇒ Object
rubocop:disable Lint/UnusedMethodArgument
326
327
328
|
# File 'lib/legion/extensions/llm/provider.rb', line 326
def resolve_model_id(model_id, config: nil) model_id
end
|
.slug ⇒ Object
298
299
300
|
# File 'lib/legion/extensions/llm/provider.rb', line 298
def slug
name.downcase
end
|
Instance Method Details
#api_base ⇒ Object
19
20
21
|
# File 'lib/legion/extensions/llm/provider.rb', line 19
def api_base
raise NotImplementedError
end
|
#assume_models_exist? ⇒ Boolean
113
114
115
|
# File 'lib/legion/extensions/llm/provider.rb', line 113
def assume_models_exist?
self.class.assume_models_exist?
end
|
#cache_instance_key ⇒ Object
283
284
285
286
287
288
289
290
291
|
# File 'lib/legion/extensions/llm/provider.rb', line 283
def cache_instance_key
if cache_local_instance?
(respond_to?(:instance_id) ? instance_id : :default).to_s
else
require 'digest'
urls = Array(config_base_url).map { |u| strip_scheme(u).downcase.chomp('/') }.sort
Digest::SHA256.hexdigest(urls.join('|'))[0, 12]
end
end
|
#cache_local_instance? ⇒ Boolean
── Cache helpers with local/shared tier selection ────────────────
252
253
254
255
256
257
|
# File 'lib/legion/extensions/llm/provider.rb', line 252
def cache_local_instance?
Array(config_base_url).any? do |url|
host = url.to_s.downcase
host.include?('localhost') || host.include?('127.0.0.1') || host.include?('::1')
end
end
|
#capabilities ⇒ Object
35
36
37
|
# File 'lib/legion/extensions/llm/provider.rb', line 35
def capabilities
self.class.capabilities
end
|
#complete(messages, tools:, temperature:, model:, params: {}, headers: {}, schema: nil, thinking: nil, tool_prefs: nil) ⇒ Object
rubocop:disable Metrics/ParameterLists
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
# File 'lib/legion/extensions/llm/provider.rb', line 44
def complete(messages, tools:, temperature:, model:, params: {}, headers: {}, schema: nil, thinking: nil,
tool_prefs: nil, &)
normalized_temperature = maybe_normalize_temperature(temperature, model)
payload = Utils.deep_merge(
render_payload(
messages,
tools: tools,
tool_prefs: tool_prefs,
temperature: normalized_temperature,
model: model,
stream: block_given?,
schema: schema,
thinking: thinking
),
params
)
if block_given?
stream_response @connection, payload, , &
else
sync_response @connection, payload,
end
end
|
#config_base_url ⇒ Object
217
218
219
|
# File 'lib/legion/extensions/llm/provider.rb', line 217
def config_base_url
respond_to?(:settings) ? settings[:base_url] : nil
end
|
#configuration_requirements ⇒ Object
39
40
41
|
# File 'lib/legion/extensions/llm/provider.rb', line 39
def configuration_requirements
self.class.configuration_requirements
end
|
101
102
103
|
# File 'lib/legion/extensions/llm/provider.rb', line 101
def configured?
configuration_requirements.all? { |req| @config.send(req) }
end
|
#embed(text, model:, dimensions:) ⇒ Object
75
76
77
78
79
|
# File 'lib/legion/extensions/llm/provider.rb', line 75
def embed(text, model:, dimensions:)
payload = render_embedding_payload(text, model:, dimensions:)
response = @connection.post(embedding_url(model:), payload)
parse_embedding_response(response, model:, text:)
end
|
#endpoint_manifest ⇒ Object
139
140
141
142
143
144
145
146
147
148
|
# File 'lib/legion/extensions/llm/provider.rb', line 139
def endpoint_manifest
endpoint_methods.each_with_object({}) do |(key, method_name), result|
next unless respond_to?(method_name)
value = public_send(method_name)
result[key] = value unless value.nil?
rescue ArgumentError, NotImplementedError
next
end
end
|
#find_reachable_url(urls) ⇒ Object
221
222
223
224
225
226
227
228
229
|
# File 'lib/legion/extensions/llm/provider.rb', line 221
def find_reachable_url(urls)
urls.each do |url|
normalized = strip_scheme(url)
scheme = tls_enabled? ? 'https' : 'http'
full = "#{scheme}://#{normalized}"
return full if url_reachable?(full)
end
nil
end
|
170
171
172
173
174
175
176
177
|
# File 'lib/legion/extensions/llm/provider.rb', line 170
def format_messages(messages)
messages.map do |msg|
{
role: msg.role.to_s,
content: msg.content
}
end
end
|
179
180
181
|
# File 'lib/legion/extensions/llm/provider.rb', line 179
def format_tool_calls(_tool_calls)
nil
end
|
23
24
25
|
# File 'lib/legion/extensions/llm/provider.rb', line 23
def
{}
end
|
#list_models ⇒ Object
rubocop:enable Metrics/ParameterLists
70
71
72
73
|
# File 'lib/legion/extensions/llm/provider.rb', line 70
def list_models
response = @connection.get models_url
parse_list_models_response response, slug, capabilities
end
|
#local? ⇒ Boolean
105
106
107
|
# File 'lib/legion/extensions/llm/provider.rb', line 105
def local?
self.class.local?
end
|
#model_allowed?(model_name) ⇒ Boolean
199
200
201
202
203
204
205
206
207
208
|
# File 'lib/legion/extensions/llm/provider.rb', line 199
def model_allowed?(model_name)
name = model_name.to_s.downcase
wl = model_whitelist
bl = model_blacklist
return false if wl.any? && wl.none? { |p| name.include?(p) }
return false if bl.any? && bl.any? { |p| name.include?(p) }
true
end
|
#model_blacklist ⇒ Object
194
195
196
197
|
# File 'lib/legion/extensions/llm/provider.rb', line 194
def model_blacklist
bl = settings[:model_blacklist] if respond_to?(:settings)
Array(bl).map { |p| p.to_s.downcase }
end
|
#model_cache_fetch(key, ttl:) ⇒ Object
275
276
277
278
279
280
281
|
# File 'lib/legion/extensions/llm/provider.rb', line 275
def model_cache_fetch(key, ttl:, &)
return yield unless defined?(Legion::Cache)
cache_local_instance? ? local_cache_fetch(key, ttl: ttl, &) : cache_fetch(key, ttl: ttl, &)
rescue StandardError
yield
end
|
#model_cache_get(key) ⇒ Object
259
260
261
262
263
264
265
|
# File 'lib/legion/extensions/llm/provider.rb', line 259
def model_cache_get(key)
return nil unless defined?(Legion::Cache)
cache_local_instance? ? local_cache_get(key) : cache_get(key)
rescue StandardError
nil
end
|
#model_cache_set(key, value, ttl:) ⇒ Object
267
268
269
270
271
272
273
|
# File 'lib/legion/extensions/llm/provider.rb', line 267
def model_cache_set(key, value, ttl:)
return unless defined?(Legion::Cache)
cache_local_instance? ? local_cache_set(key, value, ttl: ttl) : cache_set(key, value, ttl: ttl)
rescue StandardError => e
handle_exception(e, level: :debug, handled: true, operation: 'lex.provider.model_cache_set')
end
|
#model_whitelist ⇒ Object
── Model allow-list / deny-list filtering ────────────────────────
189
190
191
192
|
# File 'lib/legion/extensions/llm/provider.rb', line 189
def model_whitelist
wl = settings[:model_whitelist] if respond_to?(:settings)
Array(wl).map { |p| p.to_s.downcase }
end
|
#moderate(input, model:) ⇒ Object
81
82
83
84
85
|
# File 'lib/legion/extensions/llm/provider.rb', line 81
def moderate(input, model:)
payload = render_moderation_payload(input, model:)
response = @connection.post moderation_url, payload
parse_moderation_response(response, model:)
end
|
#name ⇒ Object
31
32
33
|
# File 'lib/legion/extensions/llm/provider.rb', line 31
def name
self.class.name
end
|
#paint(prompt, model:, size:, with: nil, mask: nil, params: {}) ⇒ Object
rubocop:disable Metrics/ParameterLists
87
88
89
90
91
92
|
# File 'lib/legion/extensions/llm/provider.rb', line 87
def paint(prompt, model:, size:, with: nil, mask: nil, params: {}) validate_paint_inputs!(with:, mask:)
payload = render_image_payload(prompt, model:, size:, with:, mask:, params:)
response = @connection.post images_url(with:, mask:), payload
parse_image_response(response, model:)
end
|
#parse_error(response) ⇒ Object
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
|
# File 'lib/legion/extensions/llm/provider.rb', line 150
def parse_error(response)
return if response.body.empty?
body = try_parse_json(response.body)
case body
when Hash
error = body['error']
return error if error.is_a?(String)
body.dig('error', 'message')
when Array
body.map do |part|
error = part['error']
error.is_a?(String) ? error : part.dig('error', 'message')
end.join('. ')
else
body
end
end
|
183
184
185
|
# File 'lib/legion/extensions/llm/provider.rb', line 183
def parse_tool_calls(_tool_calls)
nil
end
|
#readiness(live: false) ⇒ Object
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
|
# File 'lib/legion/extensions/llm/provider.rb', line 117
def readiness(live: false)
metadata = {
provider: slug.to_sym,
name: name,
configured: configured?,
ready: configured?,
local: local?,
remote: remote?,
api_base: api_base,
endpoints: endpoint_manifest,
live: live
}
return metadata.merge(health: { checked: false }) unless live && metadata[:endpoints][:health]
response = @connection.get(metadata[:endpoints][:health])
metadata.merge(ready: configured? && health_ready?(response.body), health: response.body)
rescue StandardError => e
handle_exception(e, level: :warn, handled: true, operation: 'llm.provider.readiness')
metadata.merge(ready: false, health: { error: e.class.name, message: e.message })
end
|
#remote? ⇒ Boolean
109
110
111
|
# File 'lib/legion/extensions/llm/provider.rb', line 109
def remote?
self.class.remote?
end
|
#resolve_base_url ⇒ Object
── Multi-host base_url resolution ────────────────────────────────
212
213
214
215
|
# File 'lib/legion/extensions/llm/provider.rb', line 212
def resolve_base_url
urls = Array(config_base_url)
@resolve_base_url ||= find_reachable_url(urls) || urls.first
end
|
#slug ⇒ Object
27
28
29
|
# File 'lib/legion/extensions/llm/provider.rb', line 27
def slug
self.class.slug
end
|
#strip_scheme(url) ⇒ Object
231
232
233
|
# File 'lib/legion/extensions/llm/provider.rb', line 231
def strip_scheme(url)
url.to_s.sub(%r{^https?://}, '')
end
|
#tls_enabled? ⇒ Boolean
245
246
247
248
|
# File 'lib/legion/extensions/llm/provider.rb', line 245
def tls_enabled?
tls = respond_to?(:settings) ? settings[:tls] : nil
tls.is_a?(Hash) && tls[:enabled] == true
end
|
#transcribe(audio_file, model:, language:) ⇒ Object
94
95
96
97
98
99
|
# File 'lib/legion/extensions/llm/provider.rb', line 94
def transcribe(audio_file, model:, language:, **)
file_part = build_audio_file_part(audio_file)
payload = render_transcription_payload(file_part, model:, language:, **)
response = @connection.post transcription_url, payload
parse_transcription_response(response, model:)
end
|
#url_reachable?(url) ⇒ Boolean
235
236
237
238
239
240
241
242
243
|
# File 'lib/legion/extensions/llm/provider.rb', line 235
def url_reachable?(url)
require 'uri'
require 'socket'
uri = URI.parse(url)
Socket.tcp(uri.host, uri.port, connect_timeout: 1).close
true
rescue StandardError
false
end
|