lex-llm 0.1.5 → 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 +5 -0
- data/README.md +31 -1
- 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 +2 -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: 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,10 @@
|
|
|
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
|
+
|
|
3
8
|
## 0.1.5 - 2026-04-28
|
|
4
9
|
|
|
5
10
|
- Add the expanded provider-neutral model offering contract with offering IDs, provider instances, canonical model aliases, model families, and routing metadata.
|
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.
|
|
@@ -171,6 +171,36 @@ registry.filter(
|
|
|
171
171
|
)
|
|
172
172
|
```
|
|
173
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
|
+
|
|
174
204
|
## Fleet Lanes
|
|
175
205
|
|
|
176
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.
|
|
@@ -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
|
|
@@ -39,6 +39,7 @@ module Legion
|
|
|
39
39
|
module Types
|
|
40
40
|
ModelOffering = Routing::ModelOffering unless const_defined?(:ModelOffering, false)
|
|
41
41
|
OfferingRegistry = Routing::OfferingRegistry unless const_defined?(:OfferingRegistry, false)
|
|
42
|
+
RegistryEvent = Routing::RegistryEvent unless const_defined?(:RegistryEvent, false)
|
|
42
43
|
end
|
|
43
44
|
|
|
44
45
|
# Shared routing helpers exposed under the Legion extension namespace.
|
|
@@ -46,6 +47,7 @@ module Legion
|
|
|
46
47
|
LaneKey = ::Legion::Extensions::Llm::Routing::LaneKey unless const_defined?(:LaneKey, false)
|
|
47
48
|
OfferingRegistry = ::Legion::Extensions::Llm::Routing::OfferingRegistry unless const_defined?(:OfferingRegistry,
|
|
48
49
|
false)
|
|
50
|
+
RegistryEvent = ::Legion::Extensions::Llm::Routing::RegistryEvent unless const_defined?(:RegistryEvent, false)
|
|
49
51
|
end
|
|
50
52
|
|
|
51
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
|
|
@@ -229,6 +229,7 @@ files:
|
|
|
229
229
|
- lib/legion/extensions/llm/routing/lane_key.rb
|
|
230
230
|
- lib/legion/extensions/llm/routing/model_offering.rb
|
|
231
231
|
- lib/legion/extensions/llm/routing/offering_registry.rb
|
|
232
|
+
- lib/legion/extensions/llm/routing/registry_event.rb
|
|
232
233
|
- lib/legion/extensions/llm/stream_accumulator.rb
|
|
233
234
|
- lib/legion/extensions/llm/streaming.rb
|
|
234
235
|
- lib/legion/extensions/llm/thinking.rb
|