lex-llm 0.1.3 → 0.1.4
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 +5 -0
- data/README.md +3 -1
- data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +38 -2
- data/lib/legion/extensions/llm/provider.rb +55 -0
- data/lib/legion/extensions/llm/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: acd86ca9d14268a164c9492297186706a93770c93f4e312f45d6e54cec8bd565
|
|
4
|
+
data.tar.gz: b78f52a9e0cebfedb102b7809aaf3affc20440e1fcd68a9f815e00f4a129b440
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cb2b53cfc698777af6dbfbd735a8d12c0ed5eb94a3dbea88fb91a4951946ac2c3f42790b316bfa34e9f56f822ce3c1be6a07cdc8298525f8d317895269fb2348
|
|
7
|
+
data.tar.gz: b7392fe404a9bee6f2cde122522016b43f193c4df546a69732f9d238161f61a8a6dcb93b685fc2de5a1f9703ecf2c14c433e1a790039d8489b302afbb8cce2ef
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.4 - 2026-04-28
|
|
4
|
+
|
|
5
|
+
- Add non-live provider readiness metadata for routing without expensive health or model calls by default.
|
|
6
|
+
- Map OpenAI-compatible model listings to normalized capabilities and modalities for routing.
|
|
7
|
+
|
|
3
8
|
## 0.1.3 - 2026-04-27
|
|
4
9
|
|
|
5
10
|
- Convert the gem to a standard Legion extension runtime under `Legion::Extensions::Llm`.
|
data/README.md
CHANGED
|
@@ -48,7 +48,7 @@ gem 'lex-llm'
|
|
|
48
48
|
Provider extensions should declare `lex-llm` as a gemspec dependency:
|
|
49
49
|
|
|
50
50
|
```ruby
|
|
51
|
-
spec.add_dependency 'lex-llm', '>= 0.1.
|
|
51
|
+
spec.add_dependency 'lex-llm', '>= 0.1.4'
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
For local development across LegionIO repos, prefer a local path override in the app or test `Gemfile`, not a permanent git dependency in the gemspec.
|
|
@@ -233,6 +233,8 @@ At minimum, a provider extension should define:
|
|
|
233
233
|
|
|
234
234
|
Provider extensions should avoid duplicating shared classes, schema logic, fleet lane construction, JSON handling, or common request/response objects.
|
|
235
235
|
|
|
236
|
+
All providers inherit `#readiness(live: false)`, which returns configured state, provider locality, API base, endpoint helpers, and non-live health metadata without probing remote services. Providers with a cheap health endpoint can pass `live: true` to include that endpoint response. OpenAI-compatible providers also inherit shared model-list parsing that maps discovered models into normalized capabilities and modalities for Legion routing.
|
|
237
|
+
|
|
236
238
|
## Schema Status
|
|
237
239
|
|
|
238
240
|
`lex-llm` still depends on `ruby_llm-schema` because the current schema bridge exposes:
|
|
@@ -171,18 +171,54 @@ module Legion
|
|
|
171
171
|
{}
|
|
172
172
|
end
|
|
173
173
|
|
|
174
|
-
def parse_list_models_response(response, provider,
|
|
174
|
+
def parse_list_models_response(response, provider, capabilities)
|
|
175
175
|
response.body.fetch('data', []).map do |model|
|
|
176
|
+
critical_capabilities = critical_capabilities_for(capabilities, model)
|
|
176
177
|
Legion::Extensions::Llm::Model::Info.new(
|
|
177
178
|
id: model.fetch('id'),
|
|
178
179
|
name: model['id'],
|
|
179
180
|
provider: provider,
|
|
180
|
-
created_at: model['created'],
|
|
181
|
+
created_at: model_created_at(model['created']),
|
|
182
|
+
capabilities: critical_capabilities,
|
|
183
|
+
modalities: modalities_for_capabilities(critical_capabilities),
|
|
181
184
|
metadata: model
|
|
182
185
|
)
|
|
183
186
|
end
|
|
184
187
|
end
|
|
185
188
|
|
|
189
|
+
def model_created_at(value)
|
|
190
|
+
value.is_a?(Numeric) ? Time.at(value).utc : value
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def critical_capabilities_for(capabilities, model)
|
|
194
|
+
return [] unless capabilities
|
|
195
|
+
return capabilities.critical_capabilities_for(model) if capabilities.respond_to?(:critical_capabilities_for)
|
|
196
|
+
|
|
197
|
+
{
|
|
198
|
+
'streaming' => :streaming?,
|
|
199
|
+
'function_calling' => :functions?,
|
|
200
|
+
'vision' => :vision?,
|
|
201
|
+
'embeddings' => :embeddings?,
|
|
202
|
+
'moderation' => :moderation?,
|
|
203
|
+
'image' => :images?,
|
|
204
|
+
'audio_transcription' => :audio_transcription?
|
|
205
|
+
}.filter_map do |capability, predicate|
|
|
206
|
+
capability if capabilities.respond_to?(predicate) && capabilities.public_send(predicate, model)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def modalities_for_capabilities(capabilities)
|
|
211
|
+
if capabilities.include?('embeddings') && (capabilities - ['embeddings']).empty?
|
|
212
|
+
{ input: %w[text], output: %w[embeddings] }
|
|
213
|
+
elsif capabilities.include?('image')
|
|
214
|
+
{ input: %w[text image], output: %w[image] }
|
|
215
|
+
elsif capabilities.include?('audio_transcription')
|
|
216
|
+
{ input: %w[audio], output: %w[text] }
|
|
217
|
+
else
|
|
218
|
+
{ input: %w[text image], output: %w[text] }
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
186
222
|
def render_embedding_payload(text, model:, dimensions:)
|
|
187
223
|
{ model: model, input: text, dimensions: dimensions }.compact
|
|
188
224
|
end
|
|
@@ -113,6 +113,38 @@ module Legion
|
|
|
113
113
|
self.class.assume_models_exist?
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
+
def readiness(live: false)
|
|
117
|
+
metadata = {
|
|
118
|
+
provider: slug.to_sym,
|
|
119
|
+
name: name,
|
|
120
|
+
configured: configured?,
|
|
121
|
+
ready: configured?,
|
|
122
|
+
local: local?,
|
|
123
|
+
remote: remote?,
|
|
124
|
+
api_base: api_base,
|
|
125
|
+
endpoints: endpoint_manifest,
|
|
126
|
+
live: live
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return metadata.merge(health: { checked: false }) unless live && metadata[:endpoints][:health]
|
|
130
|
+
|
|
131
|
+
response = @connection.get(metadata[:endpoints][:health])
|
|
132
|
+
metadata.merge(ready: configured? && health_ready?(response.body), health: response.body)
|
|
133
|
+
rescue StandardError => e
|
|
134
|
+
metadata.merge(ready: false, health: { error: e.class.name, message: e.message })
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def endpoint_manifest
|
|
138
|
+
endpoint_methods.each_with_object({}) do |(key, method_name), result|
|
|
139
|
+
next unless respond_to?(method_name)
|
|
140
|
+
|
|
141
|
+
value = public_send(method_name)
|
|
142
|
+
result[key] = value unless value.nil?
|
|
143
|
+
rescue ArgumentError, NotImplementedError
|
|
144
|
+
next
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
116
148
|
def parse_error(response)
|
|
117
149
|
return if response.body.empty?
|
|
118
150
|
|
|
@@ -270,6 +302,29 @@ module Legion
|
|
|
270
302
|
temperature
|
|
271
303
|
end
|
|
272
304
|
|
|
305
|
+
def endpoint_methods
|
|
306
|
+
{
|
|
307
|
+
completion: :completion_url,
|
|
308
|
+
stream: :stream_url,
|
|
309
|
+
models: :models_url,
|
|
310
|
+
embeddings: :embedding_url,
|
|
311
|
+
moderation: :moderation_url,
|
|
312
|
+
images: :images_url,
|
|
313
|
+
transcription: :transcription_url,
|
|
314
|
+
health: :health_url,
|
|
315
|
+
version: :version_url
|
|
316
|
+
}
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def health_ready?(body)
|
|
320
|
+
return body unless body.is_a?(Hash)
|
|
321
|
+
|
|
322
|
+
status = body['status'] || body[:status] || body['state'] || body[:state]
|
|
323
|
+
return true if status.nil?
|
|
324
|
+
|
|
325
|
+
%w[ok ready healthy running].include?(status.to_s.downcase)
|
|
326
|
+
end
|
|
327
|
+
|
|
273
328
|
def sync_response(connection, payload, additional_headers = {})
|
|
274
329
|
response = connection.post completion_url, payload do |req|
|
|
275
330
|
req.headers = additional_headers.merge(req.headers) unless additional_headers.empty?
|