lex-llm 0.1.4 → 0.1.6
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 +67 -3
- data/lib/legion/extensions/llm/aliases.rb +25 -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/routing/registry_event.rb +167 -0
- data/lib/legion/extensions/llm/version.rb +1 -1
- data/lib/legion/extensions/llm.rb +5 -0
- 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: d7d400d2739542ca417b189fba9d20f468d32ca6b4c1d4864fcd884a21d31577
|
|
4
|
+
data.tar.gz: 1c0ffee1ed602d77d2a295d2f4e7904abcef1ac284754553c3c5f883a78fa023
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 07ea1df46e8469e493b89855d983ef1416d38e6907404eae1502340f37f271a43c2de442825b48dbca538907042e520d85f95316803f8a87b08633edf849685a
|
|
7
|
+
data.tar.gz: 8ee001e548224a71f050c3224d140d33e652603a9308d6301e539a8f984d8aed922e7c8f8ff3313df4a42b5a693acd2dc896914a71c23e47c42eae90f4a62c9d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.6 - 2026-04-28
|
|
4
|
+
|
|
5
|
+
- Add provider-neutral registry event envelopes for future `llm.registry` offering availability, unavailability, degraded, and heartbeat publishing without persistence.
|
|
6
|
+
- Sanitize registry offering payloads and reject sensitive runtime, capacity, health, lane, and metadata keys before publication.
|
|
7
|
+
|
|
8
|
+
## 0.1.5 - 2026-04-28
|
|
9
|
+
|
|
10
|
+
- Add the expanded provider-neutral model offering contract with offering IDs, provider instances, canonical model aliases, model families, and routing metadata.
|
|
11
|
+
- Add shared model alias normalization and an in-memory offering registry for common routing filters.
|
|
12
|
+
|
|
3
13
|
## 0.1.4 - 2026-04-28
|
|
4
14
|
|
|
5
15
|
- Add non-live provider readiness metadata for routing without expensive health or model calls by default.
|
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.6'
|
|
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,75 @@ 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
|
+
|
|
174
|
+
## Registry Events
|
|
175
|
+
|
|
176
|
+
`Legion::Extensions::Llm::Routing::RegistryEvent` builds dependency-light envelopes for future `llm.registry` publishing. It does not persist registry state or publish messages by itself.
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
event = Legion::Extensions::Llm::Routing::RegistryEvent.available(
|
|
180
|
+
offering,
|
|
181
|
+
runtime: { host_id: 'macbook-m4-max', process: { pid: 12_345 } },
|
|
182
|
+
capacity: { concurrency: 4, queued: 0 },
|
|
183
|
+
health: { ready: true, latency_ms: 180 },
|
|
184
|
+
lane: offering.lane_key,
|
|
185
|
+
metadata: { observed_by: :lex_llm_ollama }
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
event.to_h
|
|
189
|
+
# => {
|
|
190
|
+
# event_id: "...",
|
|
191
|
+
# event_type: :offering_available,
|
|
192
|
+
# occurred_at: "2026-04-28T14:30:15.123456Z",
|
|
193
|
+
# offering: { ... },
|
|
194
|
+
# runtime: { host_id: "macbook-m4-max", process: { pid: 12345 } },
|
|
195
|
+
# capacity: { concurrency: 4, queued: 0 },
|
|
196
|
+
# health: { ready: true, latency_ms: 180 },
|
|
197
|
+
# lane: "llm.fleet.inference.qwen3-6-27b-q4-k-m.ctx32768",
|
|
198
|
+
# metadata: { observed_by: :lex_llm_ollama }
|
|
199
|
+
# }
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Supported event types are `:offering_available`, `:offering_unavailable`, `:offering_degraded`, and `:offering_heartbeat`. Event offerings are derived from `ModelOffering#to_h`, with sensitive offering fields removed. Optional `runtime`, `capacity`, `health`, `lane`, and `metadata` values are intended for non-secret operational context and reject sensitive keys such as credentials, tokens, secrets, URLs, endpoint paths, prompts, and reply queues.
|
|
203
|
+
|
|
140
204
|
## Fleet Lanes
|
|
141
205
|
|
|
142
206
|
Fleet routing uses shared work lanes derived from model offerings. A lane describes the work required, not the worker that happens to do it.
|
|
@@ -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
|
|
@@ -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
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Llm
|
|
6
|
+
module Routing
|
|
7
|
+
# Serializable provider-neutral envelope for future llm.registry publishing.
|
|
8
|
+
class RegistryEvent
|
|
9
|
+
EVENT_TYPES = %i[
|
|
10
|
+
offering_available
|
|
11
|
+
offering_unavailable
|
|
12
|
+
offering_degraded
|
|
13
|
+
offering_heartbeat
|
|
14
|
+
].freeze
|
|
15
|
+
SENSITIVE_KEYS = %i[
|
|
16
|
+
access_key
|
|
17
|
+
api_key
|
|
18
|
+
authorization
|
|
19
|
+
bearer
|
|
20
|
+
client_secret
|
|
21
|
+
credential
|
|
22
|
+
credentials
|
|
23
|
+
endpoint
|
|
24
|
+
endpoint_url
|
|
25
|
+
password
|
|
26
|
+
path
|
|
27
|
+
private_key
|
|
28
|
+
prompt
|
|
29
|
+
reply_to
|
|
30
|
+
secret
|
|
31
|
+
secrets
|
|
32
|
+
token
|
|
33
|
+
url
|
|
34
|
+
].freeze
|
|
35
|
+
|
|
36
|
+
attr_reader :event_id, :event_type, :occurred_at, :offering, :runtime, :capacity, :health, :lane, :metadata
|
|
37
|
+
|
|
38
|
+
class << self
|
|
39
|
+
def available(offering, **attributes)
|
|
40
|
+
new(event_type: :offering_available, offering:, **attributes)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def unavailable(offering, **attributes)
|
|
44
|
+
new(event_type: :offering_unavailable, offering:, **attributes)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def degraded(offering, **attributes)
|
|
48
|
+
new(event_type: :offering_degraded, offering:, **attributes)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def heartbeat(offering, **attributes)
|
|
52
|
+
new(event_type: :offering_heartbeat, offering:, **attributes)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def initialize(event_type:, offering:, **attributes)
|
|
57
|
+
@event_id = normalize_event_id(attributes.fetch(:event_id, SecureRandom.uuid))
|
|
58
|
+
@event_type = normalize_event_type(event_type)
|
|
59
|
+
@occurred_at = normalize_time(attributes.fetch(:occurred_at, Time.now.utc))
|
|
60
|
+
@offering = normalize_offering(offering)
|
|
61
|
+
@runtime = sanitize_optional_hash(attributes[:runtime], :runtime)
|
|
62
|
+
@capacity = sanitize_optional_hash(attributes[:capacity], :capacity)
|
|
63
|
+
@health = sanitize_optional_hash(attributes[:health], :health)
|
|
64
|
+
@lane = sanitize_optional_value(attributes[:lane], :lane)
|
|
65
|
+
@metadata = sanitize_optional_hash(attributes[:metadata], :metadata)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def to_h
|
|
69
|
+
{
|
|
70
|
+
event_id: event_id,
|
|
71
|
+
event_type: event_type,
|
|
72
|
+
occurred_at: occurred_at.utc.iso8601(6),
|
|
73
|
+
offering: sanitized_offering_hash,
|
|
74
|
+
runtime: runtime,
|
|
75
|
+
capacity: capacity,
|
|
76
|
+
health: health,
|
|
77
|
+
lane: lane,
|
|
78
|
+
metadata: metadata
|
|
79
|
+
}.compact
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def normalize_event_id(value)
|
|
85
|
+
normalized = value.to_s.strip
|
|
86
|
+
raise ArgumentError, 'event_id is required' if normalized.empty?
|
|
87
|
+
|
|
88
|
+
normalized
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def normalize_event_type(value)
|
|
92
|
+
normalized = value.to_sym
|
|
93
|
+
raise ArgumentError, "unsupported registry event type: #{value}" unless EVENT_TYPES.include?(normalized)
|
|
94
|
+
|
|
95
|
+
normalized
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def normalize_time(value)
|
|
99
|
+
return value.utc if value.respond_to?(:utc)
|
|
100
|
+
|
|
101
|
+
Time.parse(value.to_s).utc
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def normalize_offering(value)
|
|
105
|
+
return value if value.is_a?(ModelOffering)
|
|
106
|
+
|
|
107
|
+
ModelOffering.new(value)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def sanitized_offering_hash
|
|
111
|
+
sanitize_hash(offering.to_h, on_sensitive: :drop)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def sanitize_optional_hash(value, label)
|
|
115
|
+
return nil if value.nil?
|
|
116
|
+
|
|
117
|
+
sanitize_hash(value.to_h, label:)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def sanitize_optional_value(value, label)
|
|
121
|
+
return nil if value.nil?
|
|
122
|
+
return sanitize_hash(value.to_h, label:) if value.respond_to?(:to_h)
|
|
123
|
+
return value unless value.is_a?(Array)
|
|
124
|
+
|
|
125
|
+
sanitize_array(value, label:, path: [])
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def sanitize_hash(hash, label: nil, path: [], on_sensitive: :raise)
|
|
129
|
+
hash.each_with_object({}) do |(key, value), sanitized|
|
|
130
|
+
normalized_key = key.to_sym
|
|
131
|
+
key_path = path + [normalized_key]
|
|
132
|
+
if sensitive_key?(normalized_key)
|
|
133
|
+
raise_sensitive_key!(label, key_path) if on_sensitive == :raise
|
|
134
|
+
|
|
135
|
+
next
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
sanitized[normalized_key] = sanitize_value(value, label:, path: key_path, on_sensitive:)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def sanitize_array(array, label:, path:, on_sensitive: :raise)
|
|
143
|
+
array.map { |value| sanitize_value(value, label:, path:, on_sensitive:) }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def sanitize_value(value, label:, path:, on_sensitive:)
|
|
147
|
+
return sanitize_hash(value, label:, path:, on_sensitive:) if value.is_a?(Hash)
|
|
148
|
+
return sanitize_array(value, label:, path:, on_sensitive:) if value.is_a?(Array)
|
|
149
|
+
|
|
150
|
+
value
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def sensitive_key?(key)
|
|
154
|
+
normalized = key.to_s.downcase.gsub(/[^a-z0-9]+/, '_').to_sym
|
|
155
|
+
SENSITIVE_KEYS.include?(normalized) ||
|
|
156
|
+
normalized.to_s.end_with?('_key', '_secret', '_token', '_password')
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def raise_sensitive_key!(label, path)
|
|
160
|
+
prefix = label ? "#{label} contains" : 'registry event contains'
|
|
161
|
+
raise ArgumentError, "#{prefix} sensitive key: #{path.join('.')}"
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -38,11 +38,16 @@ 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)
|
|
42
|
+
RegistryEvent = Routing::RegistryEvent unless const_defined?(:RegistryEvent, false)
|
|
41
43
|
end
|
|
42
44
|
|
|
43
45
|
# Shared routing helpers exposed under the Legion extension namespace.
|
|
44
46
|
module Routing
|
|
45
47
|
LaneKey = ::Legion::Extensions::Llm::Routing::LaneKey unless const_defined?(:LaneKey, false)
|
|
48
|
+
OfferingRegistry = ::Legion::Extensions::Llm::Routing::OfferingRegistry unless const_defined?(:OfferingRegistry,
|
|
49
|
+
false)
|
|
50
|
+
RegistryEvent = ::Legion::Extensions::Llm::Routing::RegistryEvent unless const_defined?(:RegistryEvent, false)
|
|
46
51
|
end
|
|
47
52
|
|
|
48
53
|
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.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- LegionIO
|
|
@@ -228,6 +228,8 @@ 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
|
|
232
|
+
- lib/legion/extensions/llm/routing/registry_event.rb
|
|
231
233
|
- lib/legion/extensions/llm/stream_accumulator.rb
|
|
232
234
|
- lib/legion/extensions/llm/streaming.rb
|
|
233
235
|
- lib/legion/extensions/llm/thinking.rb
|