lex-llm 0.1.3 → 0.1.5
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 +10 -0
- data/README.md +39 -3
- data/lib/legion/extensions/llm/aliases.rb +25 -0
- 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/routing/lane_key.rb +8 -1
- data/lib/legion/extensions/llm/routing/model_offering.rb +43 -3
- data/lib/legion/extensions/llm/routing/offering_registry.rb +99 -0
- data/lib/legion/extensions/llm/version.rb +1 -1
- data/lib/legion/extensions/llm.rb +3 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aae636ae2e90a5bbbf5b11ba40ae3bd21ab628fa7754e0b9c78539f2535d03fc
|
|
4
|
+
data.tar.gz: 79a95d21375a4da155f768f8696d408917a8c37391ec5c986afce9dde2033f08
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 88bd2debf160491c93dbd275d332a4563f7d607142105d631114702b68cab98da103ce8b4e1b5a70f0da1aef3bc1727b384e2a1b1340bac3fe3079939e85377f
|
|
7
|
+
data.tar.gz: 49055e0945460d46444536b0fee9e1fabcba640a97f57b84486e75e5c014d765aaa1c7c33e6f67ae455fb43143b32a3fec80e0c507e20da5b29f34e714cd6d82
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.5 - 2026-04-28
|
|
4
|
+
|
|
5
|
+
- Add the expanded provider-neutral model offering contract with offering IDs, provider instances, canonical model aliases, model families, and routing metadata.
|
|
6
|
+
- Add shared model alias normalization and an in-memory offering registry for common routing filters.
|
|
7
|
+
|
|
8
|
+
## 0.1.4 - 2026-04-28
|
|
9
|
+
|
|
10
|
+
- Add non-live provider readiness metadata for routing without expensive health or model calls by default.
|
|
11
|
+
- Map OpenAI-compatible model listings to normalized capabilities and modalities for routing.
|
|
12
|
+
|
|
3
13
|
## 0.1.3 - 2026-04-27
|
|
4
14
|
|
|
5
15
|
- 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.5'
|
|
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.
|
|
@@ -90,11 +90,14 @@ A model offering describes one concrete model made available by one provider ins
|
|
|
90
90
|
|
|
91
91
|
```ruby
|
|
92
92
|
offering = Legion::Extensions::Llm::Routing::ModelOffering.new(
|
|
93
|
+
offering_id: 'ollama:macbook_m4_max:inference:qwen3-6-27b-q4-k-m',
|
|
93
94
|
provider_family: :ollama,
|
|
94
|
-
|
|
95
|
+
provider_instance: :macbook_m4_max,
|
|
95
96
|
transport: :local,
|
|
96
97
|
tier: :local,
|
|
97
98
|
model: 'qwen3.6:27b-q4_K_M',
|
|
99
|
+
canonical_model_alias: 'qwen3.6:27b-q4_K_M',
|
|
100
|
+
model_family: :qwen,
|
|
98
101
|
usage_type: :inference,
|
|
99
102
|
capabilities: %i[chat tools vision thinking],
|
|
100
103
|
limits: {
|
|
@@ -106,6 +109,10 @@ offering = Legion::Extensions::Llm::Routing::ModelOffering.new(
|
|
|
106
109
|
latency_ms: 180
|
|
107
110
|
},
|
|
108
111
|
policy_tags: %i[internal_only phi_allowed],
|
|
112
|
+
routing_metadata: {
|
|
113
|
+
region: :local,
|
|
114
|
+
accelerator: :metal
|
|
115
|
+
},
|
|
109
116
|
metadata: {
|
|
110
117
|
enabled: true,
|
|
111
118
|
eligibility: {
|
|
@@ -125,18 +132,45 @@ offering.eligible_for?(
|
|
|
125
132
|
|
|
126
133
|
Common offering fields:
|
|
127
134
|
|
|
135
|
+
- `offering_id`: stable identifier for the concrete offering; generated from provider, instance, usage type, and canonical alias when omitted
|
|
128
136
|
- `provider_family`: provider implementation family, such as `:ollama`, `:vllm`, `:bedrock`, `:anthropic`, or `:openai`
|
|
129
|
-
- `
|
|
137
|
+
- `provider_instance`: concrete provider instance, account, node, region, or local runtime
|
|
138
|
+
- `instance_id`: compatibility alias for `provider_instance`
|
|
139
|
+
- `model_family`: provider-neutral family such as `:openai`, `:anthropic`, `:gemini`, `:qwen`, or `:llama`
|
|
130
140
|
- `transport`: `:local`, `:http`, `:rabbitmq`, `:sdk`, or another provider-supported transport
|
|
131
141
|
- `tier`: `:local`, `:private`, `:fleet`, `:cloud`, `:frontier`, or deployment-specific policy tier
|
|
132
142
|
- `model`: provider model name or normalized model alias
|
|
143
|
+
- `canonical_model_alias`: provider-neutral alias used by routers and shared fleet lane keys when a provider deployment hides the base model
|
|
133
144
|
- `usage_type`: `:inference` or `:embedding`
|
|
134
145
|
- `capabilities`: normalized feature flags such as `:chat`, `:tools`, `:json_schema`, `:vision`, `:thinking`, or `:embedding`
|
|
135
146
|
- `limits`: context window, output token limits, rate limits, concurrency limits, and provider-specific bounds
|
|
136
147
|
- `health`: readiness, latency, recent failures, and provider-specific health metadata
|
|
137
148
|
- `policy_tags`: routing and compliance tags such as `:internal_only`, `:phi_allowed`, or `:hipaa`
|
|
149
|
+
- `routing_metadata`: provider-neutral scheduling metadata for routers; persistence is intentionally out of scope
|
|
138
150
|
- `metadata`: extension-specific metadata; sensitive values are excluded from fleet eligibility fingerprints
|
|
139
151
|
|
|
152
|
+
Provider gems that still pass `instance_id`, or that store `model_family`, `canonical_model_alias`, or `alias` under `metadata`, remain compatible. `ModelOffering` lifts those values into first-class readers for routers.
|
|
153
|
+
|
|
154
|
+
`Legion::Extensions::Llm::Aliases.canonical_model_alias(model, provider)` provides shared alias normalization from `aliases.json`, with an explicit model string fallback.
|
|
155
|
+
|
|
156
|
+
## Offering Registry
|
|
157
|
+
|
|
158
|
+
`Legion::Extensions::Llm::Routing::OfferingRegistry` is an in-memory index for discovered or configured offerings. It does not persist state.
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
registry = Legion::Extensions::Llm::Routing::OfferingRegistry.new
|
|
162
|
+
registry.register(offering)
|
|
163
|
+
|
|
164
|
+
registry.find(offering.offering_id)
|
|
165
|
+
registry.find_by_model_alias('qwen3.6:27b-q4_K_M')
|
|
166
|
+
registry.filter(
|
|
167
|
+
provider_family: :ollama,
|
|
168
|
+
provider_instance: :macbook_m4_max,
|
|
169
|
+
model_family: :qwen,
|
|
170
|
+
capability: :tools
|
|
171
|
+
)
|
|
172
|
+
```
|
|
173
|
+
|
|
140
174
|
## Fleet Lanes
|
|
141
175
|
|
|
142
176
|
Fleet routing uses shared work lanes derived from model offerings. A lane describes the work required, not the worker that happens to do it.
|
|
@@ -233,6 +267,8 @@ At minimum, a provider extension should define:
|
|
|
233
267
|
|
|
234
268
|
Provider extensions should avoid duplicating shared classes, schema logic, fleet lane construction, JSON handling, or common request/response objects.
|
|
235
269
|
|
|
270
|
+
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.
|
|
271
|
+
|
|
236
272
|
## Schema Status
|
|
237
273
|
|
|
238
274
|
`lex-llm` still depends on `ruby_llm-schema` because the current schema bridge exposes:
|
|
@@ -16,6 +16,23 @@ module Legion
|
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
def normalize_model_alias(model_id)
|
|
20
|
+
model_id.to_s.strip
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def canonical_model_alias(model_id, provider = nil)
|
|
24
|
+
normalized = normalize_model_alias(model_id)
|
|
25
|
+
provider_name = provider&.to_s
|
|
26
|
+
|
|
27
|
+
aliases.each do |alias_name, provider_map|
|
|
28
|
+
next unless alias_matches?(provider_map, normalized, provider_name)
|
|
29
|
+
|
|
30
|
+
return alias_name
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
normalized
|
|
34
|
+
end
|
|
35
|
+
|
|
19
36
|
def aliases
|
|
20
37
|
@aliases ||= load_aliases
|
|
21
38
|
end
|
|
@@ -35,6 +52,14 @@ module Legion
|
|
|
35
52
|
def reload!
|
|
36
53
|
@aliases = load_aliases
|
|
37
54
|
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def alias_matches?(provider_map, model_id, provider)
|
|
59
|
+
return provider_map[provider] == model_id if provider
|
|
60
|
+
|
|
61
|
+
provider_map.value?(model_id)
|
|
62
|
+
end
|
|
38
63
|
end
|
|
39
64
|
end
|
|
40
65
|
end
|
|
@@ -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?
|
|
@@ -9,7 +9,7 @@ module Legion
|
|
|
9
9
|
module_function
|
|
10
10
|
|
|
11
11
|
def for(offering, prefix: 'llm.fleet', include_context: true, include_fingerprint: false)
|
|
12
|
-
parts = [prefix, lane_kind(offering), model_slug(offering
|
|
12
|
+
parts = [prefix, lane_kind(offering), model_slug(lane_model(offering))]
|
|
13
13
|
if include_context && offering.inference? && offering.context_window
|
|
14
14
|
parts << "ctx#{offering.context_window}"
|
|
15
15
|
end
|
|
@@ -17,6 +17,13 @@ module Legion
|
|
|
17
17
|
parts.join('.')
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
def lane_model(offering)
|
|
21
|
+
return offering.canonical_model_alias if offering.respond_to?(:canonical_model_alias) &&
|
|
22
|
+
offering.canonical_model_alias.to_s != ''
|
|
23
|
+
|
|
24
|
+
offering.model
|
|
25
|
+
end
|
|
26
|
+
|
|
20
27
|
def lane_kind(offering)
|
|
21
28
|
offering.embedding? ? 'embed' : 'inference'
|
|
22
29
|
end
|
|
@@ -6,15 +6,23 @@ module Legion
|
|
|
6
6
|
module Routing
|
|
7
7
|
# Describes one concrete model made available by one provider instance.
|
|
8
8
|
class ModelOffering
|
|
9
|
-
attr_reader :
|
|
9
|
+
attr_reader :offering_id, :provider_family, :model_family, :provider_instance, :instance_id, :transport,
|
|
10
|
+
:tier, :model, :canonical_model_alias, :routing_metadata, :usage_type, :capabilities, :limits,
|
|
10
11
|
:credentials, :health, :cost, :policy_tags, :metadata
|
|
11
12
|
|
|
12
13
|
def initialize(data)
|
|
14
|
+
@metadata = normalize_hash(fetch_value(data, :metadata))
|
|
13
15
|
@provider_family = normalize_symbol(fetch_value(data, :provider_family, fetch_value(data, :provider)))
|
|
14
|
-
@
|
|
16
|
+
@model_family = normalize_symbol(fetch_value(data, :model_family, @metadata[:model_family]))
|
|
17
|
+
@provider_instance = normalize_symbol(fetch_value(data, :provider_instance,
|
|
18
|
+
fetch_value(data, :instance_id, @provider_family)))
|
|
19
|
+
@instance_id = @provider_instance
|
|
15
20
|
@transport = normalize_symbol(fetch_value(data, :transport, :http))
|
|
16
21
|
@tier = normalize_symbol(fetch_value(data, :tier, default_tier))
|
|
17
22
|
@model = fetch_value(data, :model).to_s
|
|
23
|
+
@canonical_model_alias = normalize_model_alias(fetch_value(data, :canonical_model_alias,
|
|
24
|
+
metadata_canonical_model_alias))
|
|
25
|
+
@routing_metadata = normalize_hash(fetch_value(data, :routing_metadata))
|
|
18
26
|
@usage_type = normalize_usage_type(fetch_value(data, :usage_type,
|
|
19
27
|
fetch_value(data, :type) ||
|
|
20
28
|
fetch_value(data, :kind) ||
|
|
@@ -25,7 +33,7 @@ module Legion
|
|
|
25
33
|
@health = normalize_hash(fetch_value(data, :health))
|
|
26
34
|
@cost = normalize_hash(fetch_value(data, :cost))
|
|
27
35
|
@policy_tags = normalize_array(fetch_value(data, :policy_tags)).map(&:to_sym)
|
|
28
|
-
@
|
|
36
|
+
@offering_id = normalize_offering_id(fetch_value(data, :offering_id, default_offering_id))
|
|
29
37
|
end
|
|
30
38
|
|
|
31
39
|
def enabled?
|
|
@@ -70,13 +78,23 @@ module Legion
|
|
|
70
78
|
LaneKey.eligibility_fingerprint(self)
|
|
71
79
|
end
|
|
72
80
|
|
|
81
|
+
def model_alias?(alias_name)
|
|
82
|
+
normalized = normalize_model_alias(alias_name)
|
|
83
|
+
[canonical_model_alias, model].compact.any? { |candidate| normalize_model_alias(candidate) == normalized }
|
|
84
|
+
end
|
|
85
|
+
|
|
73
86
|
def to_h
|
|
74
87
|
{
|
|
88
|
+
offering_id: offering_id,
|
|
75
89
|
provider_family: provider_family,
|
|
90
|
+
model_family: model_family,
|
|
91
|
+
provider_instance: provider_instance,
|
|
76
92
|
instance_id: instance_id,
|
|
77
93
|
transport: transport,
|
|
78
94
|
tier: tier,
|
|
79
95
|
model: model,
|
|
96
|
+
canonical_model_alias: canonical_model_alias,
|
|
97
|
+
routing_metadata: routing_metadata,
|
|
80
98
|
usage_type: usage_type,
|
|
81
99
|
capabilities: capabilities,
|
|
82
100
|
limits: limits,
|
|
@@ -166,6 +184,28 @@ module Legion
|
|
|
166
184
|
rescue ArgumentError, TypeError
|
|
167
185
|
nil
|
|
168
186
|
end
|
|
187
|
+
|
|
188
|
+
def metadata_canonical_model_alias
|
|
189
|
+
metadata[:canonical_model_alias] || metadata[:alias] ||
|
|
190
|
+
Legion::Extensions::Llm::Aliases.canonical_model_alias(@model, @provider_family)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def normalize_model_alias(value)
|
|
194
|
+
Legion::Extensions::Llm::Aliases.normalize_model_alias(value)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def normalize_offering_id(value)
|
|
198
|
+
value.to_s.strip
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def default_offering_id
|
|
202
|
+
[
|
|
203
|
+
provider_family,
|
|
204
|
+
provider_instance,
|
|
205
|
+
usage_type,
|
|
206
|
+
canonical_model_alias || model
|
|
207
|
+
].compact.map { |part| LaneKey.model_slug(part) }.join(':')
|
|
208
|
+
end
|
|
169
209
|
end
|
|
170
210
|
end
|
|
171
211
|
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Llm
|
|
6
|
+
module Routing
|
|
7
|
+
# In-memory index of provider-neutral model offerings.
|
|
8
|
+
class OfferingRegistry
|
|
9
|
+
include Enumerable
|
|
10
|
+
|
|
11
|
+
def initialize(offerings = [])
|
|
12
|
+
@offerings = []
|
|
13
|
+
Array(offerings).each { |offering| register(offering) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def register(offering)
|
|
17
|
+
normalized = normalize_offering(offering)
|
|
18
|
+
@offerings.reject! { |existing| existing.offering_id == normalized.offering_id }
|
|
19
|
+
@offerings << normalized
|
|
20
|
+
normalized
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def each(&)
|
|
24
|
+
@offerings.each(&)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def all
|
|
28
|
+
@offerings.dup
|
|
29
|
+
end
|
|
30
|
+
alias list all
|
|
31
|
+
|
|
32
|
+
def find(offering_id)
|
|
33
|
+
@offerings.find { |offering| offering.offering_id == offering_id.to_s }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def find_by_model_alias(alias_name)
|
|
37
|
+
@offerings.find { |offering| offering.model_alias?(alias_name) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def filter(**criteria)
|
|
41
|
+
@offerings.select do |offering|
|
|
42
|
+
matches_symbol?(offering.provider_family, criteria[:provider_family]) &&
|
|
43
|
+
matches_symbol?(offering.model_family, criteria[:model_family]) &&
|
|
44
|
+
matches_symbol?(offering.provider_instance, criteria[:provider_instance]) &&
|
|
45
|
+
matches_capability?(offering, criteria[:capability]) &&
|
|
46
|
+
matches_model_alias?(offering, criteria[:model_alias]) &&
|
|
47
|
+
matches_model?(offering, criteria[:model]) &&
|
|
48
|
+
matches_usage_type?(offering, criteria[:usage_type])
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def by_provider_family(provider_family)
|
|
53
|
+
filter(provider_family:)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def by_model_family(model_family)
|
|
57
|
+
filter(model_family:)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def by_provider_instance(provider_instance)
|
|
61
|
+
filter(provider_instance:)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def by_capability(capability)
|
|
65
|
+
filter(capability:)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def normalize_offering(offering)
|
|
71
|
+
return offering if offering.is_a?(ModelOffering)
|
|
72
|
+
|
|
73
|
+
ModelOffering.new(offering)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def matches_symbol?(actual, expected)
|
|
77
|
+
expected.nil? || actual == expected.to_sym
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def matches_capability?(offering, capability)
|
|
81
|
+
capability.nil? || offering.supports?(capability)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def matches_model_alias?(offering, model_alias)
|
|
85
|
+
model_alias.nil? || offering.model_alias?(model_alias)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def matches_model?(offering, model)
|
|
89
|
+
model.nil? || offering.model == model.to_s
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def matches_usage_type?(offering, usage_type)
|
|
93
|
+
usage_type.nil? || offering.usage_type == usage_type.to_sym
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -38,11 +38,14 @@ module Legion
|
|
|
38
38
|
# Provider-neutral value objects exposed under the Legion extension namespace.
|
|
39
39
|
module Types
|
|
40
40
|
ModelOffering = Routing::ModelOffering unless const_defined?(:ModelOffering, false)
|
|
41
|
+
OfferingRegistry = Routing::OfferingRegistry unless const_defined?(:OfferingRegistry, false)
|
|
41
42
|
end
|
|
42
43
|
|
|
43
44
|
# Shared routing helpers exposed under the Legion extension namespace.
|
|
44
45
|
module Routing
|
|
45
46
|
LaneKey = ::Legion::Extensions::Llm::Routing::LaneKey unless const_defined?(:LaneKey, false)
|
|
47
|
+
OfferingRegistry = ::Legion::Extensions::Llm::Routing::OfferingRegistry unless const_defined?(:OfferingRegistry,
|
|
48
|
+
false)
|
|
46
49
|
end
|
|
47
50
|
|
|
48
51
|
class << self
|
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.1.
|
|
4
|
+
version: 0.1.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- LegionIO
|
|
@@ -228,6 +228,7 @@ files:
|
|
|
228
228
|
- lib/legion/extensions/llm/routing.rb
|
|
229
229
|
- lib/legion/extensions/llm/routing/lane_key.rb
|
|
230
230
|
- lib/legion/extensions/llm/routing/model_offering.rb
|
|
231
|
+
- lib/legion/extensions/llm/routing/offering_registry.rb
|
|
231
232
|
- lib/legion/extensions/llm/stream_accumulator.rb
|
|
232
233
|
- lib/legion/extensions/llm/streaming.rb
|
|
233
234
|
- lib/legion/extensions/llm/thinking.rb
|