lex-llm 0.2.0 → 0.3.1
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 +13 -0
- data/lib/legion/extensions/llm/auto_registration.rb +56 -0
- data/lib/legion/extensions/llm/configuration.rb +2 -6
- data/lib/legion/extensions/llm/credential_sources.rb +246 -0
- data/lib/legion/extensions/llm/model/info.rb +1 -1
- data/lib/legion/extensions/llm/models.rb +37 -24
- data/lib/legion/extensions/llm/provider.rb +22 -45
- data/lib/legion/extensions/llm/version.rb +1 -1
- data/lib/legion/extensions/llm.rb +5 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 21bb44444f871870151b379672c39b043c36233ee0b7d634660a7fe021f355b6
|
|
4
|
+
data.tar.gz: 2bc64a7a18d4304179e7465c99e21fdf584a9e4dd54860b207bf8d8c87e738cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 930d418014199a5f3b34bf505555e54462e2e590c11475859221d9a83c2def586f547c8341f813cfab03d6677ddbc8a66e06edc9f36e6bb6ffea05d36e40ce0b
|
|
7
|
+
data.tar.gz: 66201e1d6405692d6da1fbb38d294b7632a0ef2ca42f1578c548746a5caeb3d3a25d1d37347e33f88d628408d962707d4ab254038cc97d0ad27b89bafa42b0e8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.1 - 2026-05-02
|
|
4
|
+
|
|
5
|
+
- Fix AutoRegistration to pass tier and capabilities metadata to Call::Registry on registration
|
|
6
|
+
|
|
7
|
+
## 0.3.0 - 2026-05-01
|
|
8
|
+
|
|
9
|
+
- Add CredentialSources helper: read-only probes for env vars, ~/.claude/settings.json, ~/.codex/auth.json, Legion::Settings, socket/HTTP probes, SHA-256 credential dedup
|
|
10
|
+
- Add AutoRegistration mixin: shared discover_instances/register_discovered_instances/rediscover! for lex-llm-* provider self-registration into Call::Registry
|
|
11
|
+
- Delete Provider.register, .resolve, .for, .providers, .local_providers, .remote_providers, .configured_providers, .configured_remote_providers — replaced by Call::Registry
|
|
12
|
+
- Delete Configuration.register_provider_options — providers accept plain Hash config via new HashConfig wrapper
|
|
13
|
+
- Provider#initialize accepts plain Hash in addition to Configuration objects
|
|
14
|
+
- Models module uses Call::Registry with namespace-scanning fallback for standalone usage
|
|
15
|
+
|
|
3
16
|
## 0.2.0 - 2026-04-30
|
|
4
17
|
|
|
5
18
|
- Promote ModelInfo Data.define value object with immutable fields: instance, parameter_count, parameter_size, quantization, size_bytes, modalities_input, modalities_output
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Llm
|
|
6
|
+
# Mixin that lex-llm-* provider modules `extend` to get shared
|
|
7
|
+
# registration boilerplate. The provider only needs to override
|
|
8
|
+
# `discover_instances` — everything else is handled here.
|
|
9
|
+
#
|
|
10
|
+
# Prerequisites on the extending module:
|
|
11
|
+
# - `PROVIDER_FAMILY` constant (Symbol, e.g. :ollama)
|
|
12
|
+
# - `provider_class` singleton method returning the Provider subclass
|
|
13
|
+
module AutoRegistration
|
|
14
|
+
# Override in each provider. Returns { instance_id => config_hash }.
|
|
15
|
+
def discover_instances
|
|
16
|
+
{}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Calls discover_instances, creates a LexLLMAdapter for each,
|
|
20
|
+
# and registers into Call::Registry.
|
|
21
|
+
#
|
|
22
|
+
# Strips :tier and :capabilities from config before passing to
|
|
23
|
+
# the adapter (these are metadata, not connection config).
|
|
24
|
+
#
|
|
25
|
+
# Guarded: no-op when Legion::LLM::Call::Registry is not loaded.
|
|
26
|
+
def register_discovered_instances
|
|
27
|
+
return unless defined?(Legion::LLM::Call::Registry)
|
|
28
|
+
|
|
29
|
+
instances = discover_instances
|
|
30
|
+
instances.each do |instance_id, config|
|
|
31
|
+
registry_config = config.except(:tier, :capabilities)
|
|
32
|
+
adapter = Legion::LLM::Call::LexLLMAdapter.new(
|
|
33
|
+
self::PROVIDER_FAMILY, provider_class, instance_config: registry_config
|
|
34
|
+
)
|
|
35
|
+
meta = { tier: config[:tier], capabilities: config[:capabilities] || [] }
|
|
36
|
+
Legion::LLM::Call::Registry.register(
|
|
37
|
+
self::PROVIDER_FAMILY, adapter, instance: instance_id, metadata: meta
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
rescue StandardError => e
|
|
41
|
+
log.warn "[#{self::PROVIDER_FAMILY}] self-registration failed: #{e.message}" if respond_to?(:log)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Deregisters all instances for this provider and re-runs discovery.
|
|
45
|
+
#
|
|
46
|
+
# Guarded: no-op when Legion::LLM::Call::Registry is not loaded.
|
|
47
|
+
def rediscover!
|
|
48
|
+
return unless defined?(Legion::LLM::Call::Registry)
|
|
49
|
+
|
|
50
|
+
Legion::LLM::Call::Registry.deregister_provider(self::PROVIDER_FAMILY)
|
|
51
|
+
register_discovered_instances
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -16,10 +16,6 @@ module Legion
|
|
|
16
16
|
defaults[key] = default
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def register_provider_options(options)
|
|
20
|
-
Array(options).each { |key| option(key, nil) }
|
|
21
|
-
end
|
|
22
|
-
|
|
23
19
|
def options
|
|
24
20
|
option_keys.dup
|
|
25
21
|
end
|
|
@@ -32,8 +28,8 @@ module Legion
|
|
|
32
28
|
end
|
|
33
29
|
|
|
34
30
|
# System-level options are declared here.
|
|
35
|
-
# Provider-specific options are declared in each provider
|
|
36
|
-
# `self.configuration_options
|
|
31
|
+
# Provider-specific options are declared in each provider extension via
|
|
32
|
+
# `self.configuration_options`.
|
|
37
33
|
option :default_model, nil
|
|
38
34
|
option :default_embedding_model, nil
|
|
39
35
|
option :default_moderation_model, nil
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
6
|
+
module Legion
|
|
7
|
+
module Extensions
|
|
8
|
+
module Llm
|
|
9
|
+
# Read-only helpers that provider gems use to probe common credential
|
|
10
|
+
# locations (env vars, Claude config, Codex auth, Legion settings, and
|
|
11
|
+
# network probes). All methods are pure readers — the calling provider
|
|
12
|
+
# decides what to do with the result.
|
|
13
|
+
module CredentialSources
|
|
14
|
+
CLAUDE_SETTINGS = File.expand_path('~/.claude/settings.json')
|
|
15
|
+
CLAUDE_PROJECT = File.join(Dir.pwd, '.claude', 'settings.json')
|
|
16
|
+
CODEX_AUTH = File.expand_path('~/.codex/auth.json')
|
|
17
|
+
|
|
18
|
+
# --- public helpers ------------------------------------------------
|
|
19
|
+
|
|
20
|
+
# Fetch an environment variable, stripping whitespace.
|
|
21
|
+
# Returns nil when the variable is unset or blank.
|
|
22
|
+
def env(key)
|
|
23
|
+
val = ENV.fetch(key, nil)
|
|
24
|
+
return nil if val.nil?
|
|
25
|
+
|
|
26
|
+
stripped = val.strip
|
|
27
|
+
stripped.empty? ? nil : stripped
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Merged Claude config (user-level + project-level). Project settings
|
|
31
|
+
# override user settings. Memoized for the lifetime of the process.
|
|
32
|
+
def claude_config
|
|
33
|
+
@claude_config ||= merge_claude_configs
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Read a single key from the merged Claude config, trying both symbol
|
|
37
|
+
# and string variants.
|
|
38
|
+
def claude_config_value(key)
|
|
39
|
+
cfg = claude_config
|
|
40
|
+
cfg[key.to_sym] || cfg[key.to_s]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Read a key from the :env hash inside Claude config, trying both
|
|
44
|
+
# symbol and string variants.
|
|
45
|
+
def claude_env_value(key)
|
|
46
|
+
env_hash = claude_config_value(:env)
|
|
47
|
+
return nil unless env_hash.is_a?(Hash)
|
|
48
|
+
|
|
49
|
+
env_hash[key.to_sym] || env_hash[key.to_s]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Read the bearer token from ~/.codex/auth.json when auth_mode is
|
|
53
|
+
# "chatgpt" and the JWT is not expired.
|
|
54
|
+
def codex_token
|
|
55
|
+
data = read_json(CODEX_AUTH)
|
|
56
|
+
mode = data[:auth_mode] || data['auth_mode']
|
|
57
|
+
return nil unless mode == 'chatgpt'
|
|
58
|
+
|
|
59
|
+
token = data[:bearer_token] || data['bearer_token']
|
|
60
|
+
return nil if token.nil? || token.to_s.strip.empty?
|
|
61
|
+
return nil unless token_valid?(token)
|
|
62
|
+
|
|
63
|
+
token
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Read the OPENAI_API_KEY from ~/.codex/auth.json.
|
|
67
|
+
def codex_openai_key
|
|
68
|
+
data = read_json(CODEX_AUTH)
|
|
69
|
+
val = data[:OPENAI_API_KEY] || data['OPENAI_API_KEY']
|
|
70
|
+
return nil if val.nil?
|
|
71
|
+
|
|
72
|
+
stripped = val.to_s.strip
|
|
73
|
+
stripped.empty? ? nil : stripped
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Dig into Legion::Settings, returning nil if the module is not loaded
|
|
77
|
+
# or the path doesn't exist.
|
|
78
|
+
def setting(*path)
|
|
79
|
+
return nil unless defined?(::Legion::Settings)
|
|
80
|
+
|
|
81
|
+
::Legion::Settings.dig(*path)
|
|
82
|
+
rescue StandardError
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# TCP connect probe with a short timeout. Returns true if the port
|
|
87
|
+
# is reachable, false otherwise.
|
|
88
|
+
def socket_open?(host, port, timeout: 0.1)
|
|
89
|
+
require 'socket'
|
|
90
|
+
|
|
91
|
+
addr = Socket.sockaddr_in(port, host)
|
|
92
|
+
sock = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
|
|
93
|
+
sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
|
|
94
|
+
|
|
95
|
+
begin
|
|
96
|
+
sock.connect_nonblock(addr)
|
|
97
|
+
rescue IO::WaitWritable
|
|
98
|
+
return false unless sock.wait_writable(timeout)
|
|
99
|
+
|
|
100
|
+
begin
|
|
101
|
+
sock.connect_nonblock(addr)
|
|
102
|
+
rescue Errno::EISCONN
|
|
103
|
+
# already connected — success
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
true
|
|
107
|
+
rescue StandardError
|
|
108
|
+
false
|
|
109
|
+
ensure
|
|
110
|
+
sock&.close
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# HTTP GET probe via Faraday. Returns true only on a 2xx status.
|
|
114
|
+
def http_ok?(url, path:, timeout: 2)
|
|
115
|
+
require 'faraday'
|
|
116
|
+
|
|
117
|
+
conn = Faraday.new(url: url) do |f|
|
|
118
|
+
f.options.timeout = timeout
|
|
119
|
+
f.options.open_timeout = timeout
|
|
120
|
+
end
|
|
121
|
+
response = conn.get(path)
|
|
122
|
+
response.status >= 200 && response.status < 300
|
|
123
|
+
rescue StandardError
|
|
124
|
+
false
|
|
125
|
+
ensure
|
|
126
|
+
conn&.close if conn.respond_to?(:close)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Deduplicate credential configs by the SHA-256 of their credential
|
|
130
|
+
# value (api_key / bearer_token / access_token). First source wins.
|
|
131
|
+
# Entries without a credential value are always kept.
|
|
132
|
+
def dedup_credentials(candidates)
|
|
133
|
+
seen = {}
|
|
134
|
+
result = {}
|
|
135
|
+
|
|
136
|
+
candidates.each do |instance_id, config|
|
|
137
|
+
hash = credential_hash(config)
|
|
138
|
+
if hash.nil?
|
|
139
|
+
result[instance_id] = config
|
|
140
|
+
elsif !seen.key?(hash)
|
|
141
|
+
seen[hash] = instance_id
|
|
142
|
+
result[instance_id] = config
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
result
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# SHA-256 hex digest of the first credential value found in the config
|
|
150
|
+
# hash (checks api_key, bearer_token, access_token in order).
|
|
151
|
+
# Returns nil when no credential field is present.
|
|
152
|
+
def credential_hash(config)
|
|
153
|
+
val = config[:api_key] || config['api_key'] ||
|
|
154
|
+
config[:bearer_token] || config['bearer_token'] ||
|
|
155
|
+
config[:access_token] || config['access_token']
|
|
156
|
+
return nil if val.nil?
|
|
157
|
+
|
|
158
|
+
Digest::SHA256.hexdigest(val.to_s)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Returns true when the URL points to localhost / 127.0.0.1 / ::1.
|
|
162
|
+
def localhost?(url)
|
|
163
|
+
return false if url.nil?
|
|
164
|
+
|
|
165
|
+
uri = URI.parse(url.to_s)
|
|
166
|
+
host = uri.host
|
|
167
|
+
return false if host.nil?
|
|
168
|
+
|
|
169
|
+
normalized = host.delete_prefix('[').delete_suffix(']')
|
|
170
|
+
%w[localhost 127.0.0.1 ::1].include?(normalized)
|
|
171
|
+
rescue URI::InvalidURIError
|
|
172
|
+
false
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
module_function :env, :claude_config, :claude_config_value,
|
|
176
|
+
:claude_env_value, :codex_token, :codex_openai_key,
|
|
177
|
+
:setting, :socket_open?, :http_ok?,
|
|
178
|
+
:dedup_credentials, :credential_hash, :localhost?
|
|
179
|
+
|
|
180
|
+
# --- private helpers -----------------------------------------------
|
|
181
|
+
|
|
182
|
+
# Merge user-level (~/.claude/settings.json) and project-level
|
|
183
|
+
# (.claude/settings.json) Claude configs. Project overrides user.
|
|
184
|
+
def merge_claude_configs
|
|
185
|
+
user = read_json(CLAUDE_SETTINGS)
|
|
186
|
+
project = read_json(CLAUDE_PROJECT)
|
|
187
|
+
deep_merge(user, project)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Read and parse a JSON file. Returns an empty hash on any error.
|
|
191
|
+
def read_json(path)
|
|
192
|
+
return {} unless File.exist?(path)
|
|
193
|
+
|
|
194
|
+
raw = File.read(path)
|
|
195
|
+
return {} if raw.strip.empty?
|
|
196
|
+
|
|
197
|
+
if defined?(::Legion::JSON)
|
|
198
|
+
::Legion::JSON.parse(raw, symbolize_names: true)
|
|
199
|
+
else
|
|
200
|
+
::JSON.parse(raw, symbolize_names: true)
|
|
201
|
+
end
|
|
202
|
+
rescue StandardError
|
|
203
|
+
{}
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# JWT expiry check. Decodes the base64 payload segment and checks
|
|
207
|
+
# that exp > now. Returns true on any parse error (benefit of the
|
|
208
|
+
# doubt).
|
|
209
|
+
def token_valid?(token)
|
|
210
|
+
return true if token.nil?
|
|
211
|
+
|
|
212
|
+
require 'base64'
|
|
213
|
+
require 'json'
|
|
214
|
+
|
|
215
|
+
parts = token.to_s.split('.')
|
|
216
|
+
return true unless parts.length >= 2
|
|
217
|
+
|
|
218
|
+
payload = ::JSON.parse(Base64.urlsafe_decode64(parts[1]))
|
|
219
|
+
exp = payload['exp']
|
|
220
|
+
return true if exp.nil?
|
|
221
|
+
|
|
222
|
+
exp.to_i > Time.now.to_i
|
|
223
|
+
rescue StandardError
|
|
224
|
+
true
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Simple recursive hash merge (project values override user values).
|
|
228
|
+
def deep_merge(base, override)
|
|
229
|
+
base.merge(override) do |_key, old_val, new_val|
|
|
230
|
+
if old_val.is_a?(Hash) && new_val.is_a?(Hash)
|
|
231
|
+
deep_merge(old_val, new_val)
|
|
232
|
+
else
|
|
233
|
+
new_val
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
module_function :merge_claude_configs, :read_json,
|
|
239
|
+
:token_valid?, :deep_merge
|
|
240
|
+
|
|
241
|
+
private_class_method :merge_claude_configs, :read_json,
|
|
242
|
+
:token_valid?, :deep_merge
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
@@ -37,6 +37,27 @@ module Legion
|
|
|
37
37
|
class << self
|
|
38
38
|
include Legion::Logging::Helper
|
|
39
39
|
|
|
40
|
+
# Discover provider classes from the Llm namespace.
|
|
41
|
+
# Each lex-llm-* extension defines a module under Legion::Extensions::Llm
|
|
42
|
+
# that responds to `provider_class` and has a `PROVIDER_FAMILY` constant.
|
|
43
|
+
def scan_provider_classes
|
|
44
|
+
Legion::Extensions::Llm.constants(false).filter_map do |const_name|
|
|
45
|
+
mod = Legion::Extensions::Llm.const_get(const_name, false)
|
|
46
|
+
next unless mod.is_a?(Module) && mod.respond_to?(:provider_class) &&
|
|
47
|
+
mod.const_defined?(:PROVIDER_FAMILY, false)
|
|
48
|
+
|
|
49
|
+
[mod::PROVIDER_FAMILY.to_sym, mod.provider_class]
|
|
50
|
+
end.to_h
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Resolve a single provider class by slug.
|
|
54
|
+
# Returns nil when the provider is unknown.
|
|
55
|
+
def resolve_provider_class(name)
|
|
56
|
+
return nil if name.nil?
|
|
57
|
+
|
|
58
|
+
scan_provider_classes[name.to_sym]
|
|
59
|
+
end
|
|
60
|
+
|
|
40
61
|
def instance
|
|
41
62
|
@instance ||= new
|
|
42
63
|
end
|
|
@@ -71,15 +92,12 @@ module Legion
|
|
|
71
92
|
@instance = new(merged_models)
|
|
72
93
|
end
|
|
73
94
|
|
|
74
|
-
def fetch_provider_models(remote_only: true)
|
|
95
|
+
def fetch_provider_models(remote_only: true)
|
|
75
96
|
config = Legion::Extensions::Llm.config
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
Provider.configured_providers(config)
|
|
81
|
-
end
|
|
82
|
-
configured = configured_classes.select { |klass| provider_classes.include?(klass) }
|
|
97
|
+
all_providers = scan_provider_classes.values
|
|
98
|
+
provider_classes = remote_only ? all_providers.reject(&:local?) : all_providers
|
|
99
|
+
configured = provider_classes.select { |klass| klass.configured?(config) }
|
|
100
|
+
|
|
83
101
|
result = {
|
|
84
102
|
models: [],
|
|
85
103
|
fetched_providers: [],
|
|
@@ -87,18 +105,13 @@ module Legion
|
|
|
87
105
|
failed: []
|
|
88
106
|
}
|
|
89
107
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
rescue StandardError => e
|
|
98
|
-
handle_exception(e, level: :warn, handled: true,
|
|
99
|
-
operation: 'llm.models.fetch_provider_models')
|
|
100
|
-
result[:failed] << { name: provider_class.name, slug: provider_class.slug, error: e }
|
|
101
|
-
end
|
|
108
|
+
configured.each do |provider_class|
|
|
109
|
+
result[:models].concat(provider_class.new(config).list_models)
|
|
110
|
+
result[:fetched_providers] << provider_class.slug
|
|
111
|
+
rescue StandardError => e
|
|
112
|
+
handle_exception(e, level: :warn, handled: true,
|
|
113
|
+
operation: 'llm.models.fetch_provider_models')
|
|
114
|
+
result[:failed] << { name: provider_class.name, slug: provider_class.slug, error: e }
|
|
102
115
|
end
|
|
103
116
|
|
|
104
117
|
result[:fetched_providers].uniq!
|
|
@@ -112,7 +125,7 @@ module Legion
|
|
|
112
125
|
|
|
113
126
|
def resolve(model_id, provider: nil, assume_exists: false, config: nil) # rubocop:disable Metrics/PerceivedComplexity
|
|
114
127
|
config ||= Legion::Extensions::Llm.config
|
|
115
|
-
provider_class = provider ?
|
|
128
|
+
provider_class = provider ? resolve_provider_class(provider) : nil
|
|
116
129
|
|
|
117
130
|
if provider_class
|
|
118
131
|
temp_instance = provider_class.new(config)
|
|
@@ -136,8 +149,8 @@ module Legion
|
|
|
136
149
|
model ||= Model::Info.default(model_id, provider_instance.slug)
|
|
137
150
|
else
|
|
138
151
|
model = Models.find model_id, provider
|
|
139
|
-
provider_class =
|
|
140
|
-
|
|
152
|
+
provider_class = resolve_provider_class(model.provider) || raise(Error,
|
|
153
|
+
"Unknown provider: #{model.provider}")
|
|
141
154
|
provider_instance = provider_class.new(config)
|
|
142
155
|
end
|
|
143
156
|
[model, provider_instance]
|
|
@@ -486,7 +499,7 @@ module Legion
|
|
|
486
499
|
end
|
|
487
500
|
|
|
488
501
|
def provider_resolved_model_id(model_id, provider)
|
|
489
|
-
provider_class =
|
|
502
|
+
provider_class = self.class.resolve_provider_class(provider)
|
|
490
503
|
return model_id unless provider_class
|
|
491
504
|
|
|
492
505
|
provider_class.resolve_model_id(model_id, config: Legion::Extensions::Llm.config)
|
|
@@ -3,6 +3,27 @@
|
|
|
3
3
|
module Legion
|
|
4
4
|
module Extensions
|
|
5
5
|
module Llm
|
|
6
|
+
# Lightweight wrapper that lets a plain Hash behave like a Configuration
|
|
7
|
+
# object, responding to method-style accessors (e.g. +config.api_key+).
|
|
8
|
+
class HashConfig
|
|
9
|
+
def initialize(hash)
|
|
10
|
+
@data = hash.transform_keys(&:to_sym)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def respond_to_missing?(name, include_private = false)
|
|
14
|
+
@data.key?(name.to_sym) || super
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def method_missing(name, *args)
|
|
18
|
+
key = name.to_sym
|
|
19
|
+
if name.to_s.end_with?('=')
|
|
20
|
+
@data[name.to_s.chomp('=').to_sym] = args.first
|
|
21
|
+
elsif @data.key?(key)
|
|
22
|
+
@data[key]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
6
27
|
# Base class for LLM providers.
|
|
7
28
|
class Provider
|
|
8
29
|
include Streaming
|
|
@@ -11,7 +32,7 @@ module Legion
|
|
|
11
32
|
attr_reader :config, :connection
|
|
12
33
|
|
|
13
34
|
def initialize(config)
|
|
14
|
-
@config = config
|
|
35
|
+
@config = config.is_a?(Hash) ? HashConfig.new(config) : config
|
|
15
36
|
ensure_configured!
|
|
16
37
|
@connection = Connection.new(self, @config)
|
|
17
38
|
end
|
|
@@ -338,50 +359,6 @@ module Legion
|
|
|
338
359
|
def configured?(config)
|
|
339
360
|
configuration_requirements.all? { |req| config.send(req) }
|
|
340
361
|
end
|
|
341
|
-
|
|
342
|
-
# @deprecated Use the extension registry instead. Will be removed in 1.0.
|
|
343
|
-
def register(name, provider_class)
|
|
344
|
-
providers[name.to_sym] = provider_class
|
|
345
|
-
Legion::Extensions::Llm::Configuration.register_provider_options(provider_class.configuration_options)
|
|
346
|
-
end
|
|
347
|
-
|
|
348
|
-
# @deprecated Use the extension registry instead. Will be removed in 1.0.
|
|
349
|
-
def resolve(name)
|
|
350
|
-
return nil if name.nil?
|
|
351
|
-
|
|
352
|
-
providers[name.to_sym]
|
|
353
|
-
end
|
|
354
|
-
|
|
355
|
-
# @deprecated Use the extension registry instead. Will be removed in 1.0.
|
|
356
|
-
def for(model)
|
|
357
|
-
model_info = Models.find(model)
|
|
358
|
-
resolve model_info.provider
|
|
359
|
-
end
|
|
360
|
-
|
|
361
|
-
# @deprecated Use the extension registry instead. Will be removed in 1.0.
|
|
362
|
-
def providers
|
|
363
|
-
@providers ||= {}
|
|
364
|
-
end
|
|
365
|
-
|
|
366
|
-
def local_providers
|
|
367
|
-
providers.select { |_slug, provider_class| provider_class.local? }
|
|
368
|
-
end
|
|
369
|
-
|
|
370
|
-
def remote_providers
|
|
371
|
-
providers.select { |_slug, provider_class| provider_class.remote? }
|
|
372
|
-
end
|
|
373
|
-
|
|
374
|
-
def configured_providers(config)
|
|
375
|
-
providers.select do |_slug, provider_class|
|
|
376
|
-
provider_class.configured?(config)
|
|
377
|
-
end.values
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
def configured_remote_providers(config)
|
|
381
|
-
providers.select do |_slug, provider_class|
|
|
382
|
-
provider_class.remote? && provider_class.configured?(config)
|
|
383
|
-
end.values
|
|
384
|
-
end
|
|
385
362
|
end
|
|
386
363
|
|
|
387
364
|
private
|
|
@@ -31,6 +31,8 @@ module Legion
|
|
|
31
31
|
'ui' => 'UI'
|
|
32
32
|
)
|
|
33
33
|
loader.ignore("#{__dir__}/llm/version.rb")
|
|
34
|
+
loader.ignore("#{__dir__}/llm/auto_registration.rb")
|
|
35
|
+
loader.ignore("#{__dir__}/llm/credential_sources.rb")
|
|
34
36
|
loader.ignore("#{__dir__}/llm/transport/exchanges")
|
|
35
37
|
loader.ignore("#{__dir__}/llm/transport/messages")
|
|
36
38
|
loader.push_dir("#{__dir__}/llm", namespace: self)
|
|
@@ -85,7 +87,7 @@ module Legion
|
|
|
85
87
|
end
|
|
86
88
|
|
|
87
89
|
def providers
|
|
88
|
-
|
|
90
|
+
Models.scan_provider_classes.values
|
|
89
91
|
end
|
|
90
92
|
|
|
91
93
|
def configure
|
|
@@ -131,6 +133,8 @@ module Legion
|
|
|
131
133
|
ProviderSettings.build(...)
|
|
132
134
|
end
|
|
133
135
|
|
|
136
|
+
require_relative 'llm/auto_registration'
|
|
137
|
+
require_relative 'llm/credential_sources'
|
|
134
138
|
loader.eager_load
|
|
135
139
|
end
|
|
136
140
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-llm
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- LegionIO
|
|
@@ -201,12 +201,14 @@ files:
|
|
|
201
201
|
- lib/legion/extensions/llm/aliases.json
|
|
202
202
|
- lib/legion/extensions/llm/aliases.rb
|
|
203
203
|
- lib/legion/extensions/llm/attachment.rb
|
|
204
|
+
- lib/legion/extensions/llm/auto_registration.rb
|
|
204
205
|
- lib/legion/extensions/llm/chat.rb
|
|
205
206
|
- lib/legion/extensions/llm/chunk.rb
|
|
206
207
|
- lib/legion/extensions/llm/configuration.rb
|
|
207
208
|
- lib/legion/extensions/llm/connection.rb
|
|
208
209
|
- lib/legion/extensions/llm/content.rb
|
|
209
210
|
- lib/legion/extensions/llm/context.rb
|
|
211
|
+
- lib/legion/extensions/llm/credential_sources.rb
|
|
210
212
|
- lib/legion/extensions/llm/embedding.rb
|
|
211
213
|
- lib/legion/extensions/llm/error.rb
|
|
212
214
|
- lib/legion/extensions/llm/image.rb
|