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
-
#normalize_url(url) ⇒ 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.
34
35
36
37
38
|
# File 'lib/legion/extensions/llm/provider.rb', line 34
def initialize(config)
@config = config.is_a?(Hash) ? HashConfig.new(config) : config
ensure_configured!
@connection = Connection.new(self, @config)
end
|
Instance Attribute Details
#config ⇒ Object
Returns the value of attribute config.
32
33
34
|
# File 'lib/legion/extensions/llm/provider.rb', line 32
def config
@config
end
|
#connection ⇒ Object
Returns the value of attribute connection.
32
33
34
|
# File 'lib/legion/extensions/llm/provider.rb', line 32
def connection
@connection
end
|
Class Method Details
.assume_models_exist? ⇒ Boolean
351
352
353
|
# File 'lib/legion/extensions/llm/provider.rb', line 351
def assume_models_exist?
false
end
|
.capabilities ⇒ Object
331
332
333
|
# File 'lib/legion/extensions/llm/provider.rb', line 331
def capabilities
nil
end
|
.configuration_options ⇒ Object
339
340
341
|
# File 'lib/legion/extensions/llm/provider.rb', line 339
def configuration_options
[]
end
|
.configuration_requirements ⇒ Object
335
336
337
|
# File 'lib/legion/extensions/llm/provider.rb', line 335
def configuration_requirements
[]
end
|
359
360
361
|
# File 'lib/legion/extensions/llm/provider.rb', line 359
def configured?(config)
configuration_requirements.all? { |req| config.send(req) }
end
|
.local? ⇒ Boolean
343
344
345
|
# File 'lib/legion/extensions/llm/provider.rb', line 343
def local?
false
end
|
.name ⇒ Object
323
324
325
|
# File 'lib/legion/extensions/llm/provider.rb', line 323
def name
to_s.split('::').last
end
|
.remote? ⇒ Boolean
347
348
349
|
# File 'lib/legion/extensions/llm/provider.rb', line 347
def remote?
!local?
end
|
.resolve_model_id(model_id, config: nil) ⇒ Object
rubocop:disable Lint/UnusedMethodArgument
355
356
357
|
# File 'lib/legion/extensions/llm/provider.rb', line 355
def resolve_model_id(model_id, config: nil) model_id
end
|
.slug ⇒ Object
327
328
329
|
# File 'lib/legion/extensions/llm/provider.rb', line 327
def slug
name.downcase
end
|
Instance Method Details
#api_base ⇒ Object
40
41
42
|
# File 'lib/legion/extensions/llm/provider.rb', line 40
def api_base
raise NotImplementedError
end
|
#assume_models_exist? ⇒ Boolean
134
135
136
|
# File 'lib/legion/extensions/llm/provider.rb', line 134
def assume_models_exist?
self.class.assume_models_exist?
end
|
#cache_instance_key ⇒ Object
312
313
314
315
316
317
318
319
320
|
# File 'lib/legion/extensions/llm/provider.rb', line 312
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 ────────────────
281
282
283
284
285
286
|
# File 'lib/legion/extensions/llm/provider.rb', line 281
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
56
57
58
|
# File 'lib/legion/extensions/llm/provider.rb', line 56
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
|
# File 'lib/legion/extensions/llm/provider.rb', line 65
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
240
241
242
|
# File 'lib/legion/extensions/llm/provider.rb', line 240
def config_base_url
respond_to?(:settings) ? settings[:base_url] : nil
end
|
#configuration_requirements ⇒ Object
60
61
62
|
# File 'lib/legion/extensions/llm/provider.rb', line 60
def configuration_requirements
self.class.configuration_requirements
end
|
122
123
124
|
# File 'lib/legion/extensions/llm/provider.rb', line 122
def configured?
configuration_requirements.all? { |req| @config.send(req) }
end
|
#embed(text, model:, dimensions:) ⇒ Object
96
97
98
99
100
|
# File 'lib/legion/extensions/llm/provider.rb', line 96
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
160
161
162
163
164
165
166
167
168
169
|
# File 'lib/legion/extensions/llm/provider.rb', line 160
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
252
253
254
255
256
257
258
|
# File 'lib/legion/extensions/llm/provider.rb', line 252
def find_reachable_url(urls)
urls.each do |url|
full = normalize_url(url)
return full if url_reachable?(full)
end
nil
end
|
191
192
193
194
195
196
197
198
|
# File 'lib/legion/extensions/llm/provider.rb', line 191
def format_messages(messages)
messages.map do |msg|
{
role: msg.role.to_s,
content: msg.content
}
end
end
|
200
201
202
|
# File 'lib/legion/extensions/llm/provider.rb', line 200
def format_tool_calls(_tool_calls)
nil
end
|
44
45
46
|
# File 'lib/legion/extensions/llm/provider.rb', line 44
def
{}
end
|
#list_models ⇒ Object
rubocop:enable Metrics/ParameterLists
91
92
93
94
|
# File 'lib/legion/extensions/llm/provider.rb', line 91
def list_models
response = @connection.get models_url
parse_list_models_response response, slug, capabilities
end
|
#local? ⇒ Boolean
126
127
128
|
# File 'lib/legion/extensions/llm/provider.rb', line 126
def local?
self.class.local?
end
|
#model_allowed?(model_name) ⇒ Boolean
220
221
222
223
224
225
226
227
228
229
|
# File 'lib/legion/extensions/llm/provider.rb', line 220
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
215
216
217
218
|
# File 'lib/legion/extensions/llm/provider.rb', line 215
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
304
305
306
307
308
309
310
|
# File 'lib/legion/extensions/llm/provider.rb', line 304
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
288
289
290
291
292
293
294
|
# File 'lib/legion/extensions/llm/provider.rb', line 288
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
296
297
298
299
300
301
302
|
# File 'lib/legion/extensions/llm/provider.rb', line 296
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 ────────────────────────
210
211
212
213
|
# File 'lib/legion/extensions/llm/provider.rb', line 210
def model_whitelist
wl = settings[:model_whitelist] if respond_to?(:settings)
Array(wl).map { |p| p.to_s.downcase }
end
|
#moderate(input, model:) ⇒ Object
102
103
104
105
106
|
# File 'lib/legion/extensions/llm/provider.rb', line 102
def moderate(input, model:)
payload = render_moderation_payload(input, model:)
response = @connection.post moderation_url, payload
parse_moderation_response(response, model:)
end
|
#name ⇒ Object
52
53
54
|
# File 'lib/legion/extensions/llm/provider.rb', line 52
def name
self.class.name
end
|
#normalize_url(url) ⇒ Object
244
245
246
247
248
249
250
|
# File 'lib/legion/extensions/llm/provider.rb', line 244
def normalize_url(url)
raw = url.to_s.strip
return raw if raw.match?(%r{^https?://})
scheme = tls_enabled? ? 'https' : 'http'
"#{scheme}://#{raw}"
end
|
#paint(prompt, model:, size:, with: nil, mask: nil, params: {}) ⇒ Object
rubocop:disable Metrics/ParameterLists
108
109
110
111
112
113
|
# File 'lib/legion/extensions/llm/provider.rb', line 108
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
|
# File 'lib/legion/extensions/llm/provider.rb', line 171
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
|
204
205
206
|
# File 'lib/legion/extensions/llm/provider.rb', line 204
def parse_tool_calls(_tool_calls)
nil
end
|
#readiness(live: false) ⇒ Object
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
|
# File 'lib/legion/extensions/llm/provider.rb', line 138
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
130
131
132
|
# File 'lib/legion/extensions/llm/provider.rb', line 130
def remote?
self.class.remote?
end
|
#resolve_base_url ⇒ Object
── Multi-host base_url resolution ────────────────────────────────
233
234
235
236
237
238
|
# File 'lib/legion/extensions/llm/provider.rb', line 233
def resolve_base_url
urls = Array(config_base_url)
return nil if urls.empty?
@resolve_base_url ||= find_reachable_url(urls) || normalize_url(urls.first)
end
|
#slug ⇒ Object
48
49
50
|
# File 'lib/legion/extensions/llm/provider.rb', line 48
def slug
self.class.slug
end
|
#strip_scheme(url) ⇒ Object
260
261
262
|
# File 'lib/legion/extensions/llm/provider.rb', line 260
def strip_scheme(url)
url.to_s.sub(%r{^https?://}, '')
end
|
#tls_enabled? ⇒ Boolean
274
275
276
277
|
# File 'lib/legion/extensions/llm/provider.rb', line 274
def tls_enabled?
tls = respond_to?(:settings) ? settings[:tls] : nil
tls.is_a?(Hash) && tls[:enabled] == true
end
|
#transcribe(audio_file, model:, language:) ⇒ Object
115
116
117
118
119
120
|
# File 'lib/legion/extensions/llm/provider.rb', line 115
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
264
265
266
267
268
269
270
271
272
|
# File 'lib/legion/extensions/llm/provider.rb', line 264
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
|