lex-llm 0.7.3 → 0.7.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 +19 -0
- data/lib/legion/extensions/llm/fleet/provider_responder.rb +4 -1
- data/lib/legion/extensions/llm/fleet/worker_execution.rb +5 -1
- data/lib/legion/extensions/llm/inventory/records.rb +56 -9
- data/lib/legion/extensions/llm/inventory/registry.rb +4 -2
- data/lib/legion/extensions/llm/inventory/scoped_refresher.rb +1 -2
- data/lib/legion/extensions/llm/inventory/weight_reconciler.rb +242 -0
- data/lib/legion/extensions/llm/inventory/weight_schema.rb +107 -0
- data/lib/legion/extensions/llm/taxonomies.rb +24 -0
- data/lib/legion/extensions/llm/version.rb +1 -1
- data/lib/legion/extensions/llm.rb +2 -0
- data/spec/legion/extensions/llm/fleet/provider_responder_spec.rb +33 -0
- data/spec/legion/extensions/llm/fleet/worker_execution_spec.rb +20 -0
- data/spec/legion/extensions/llm/inventory/records_spec.rb +60 -7
- data/spec/legion/extensions/llm/inventory/registry_activation_spec.rb +18 -0
- data/spec/legion/extensions/llm/inventory/scoped_refresher_spec.rb +3 -0
- data/spec/legion/extensions/llm/inventory/weight_reconciler_spec.rb +308 -0
- data/spec/legion/extensions/llm/inventory/weight_schema_spec.rb +91 -0
- data/spec/legion/extensions/llm/taxonomies_spec.rb +27 -0
- data/spec/support/ssot_registry_helpers.rb +3 -2
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b11032eff9bab4fc8c4ad457cd78fdce95f6ec245a5938dbd6ea987729db4238
|
|
4
|
+
data.tar.gz: f799901fe6f05201a25237aeafb7779538a995e63e5117e18550935b9e77e12e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d8f610a4c4bb9fdf202c633360c689082f974033e27bdb9bb5c15afa8b609fee313cb36e32109c829b2f6a3613ae64e6859fc962536c804ec3d30f9e47bbc959
|
|
7
|
+
data.tar.gz: 82b0d2428591452f708a4fbccd9be04c40752cd3b2a5989e0e0df220406a9e6cfe103da82ad9c5d7123d11875bec97f01ad8c81b83e7ca21deac37b4fde03c98
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.6 - 2026-08-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Shared writer-cadence weight reconciliation.** `Inventory::WeightReconciler` atomically rebuilds write-time weights, publishes changed snapshots, protects unpublished activation state, and keeps cache/sequence mutation behind each writer's existing mutex. `DormantWeightTracker` reports configured weights with no published lane once per absence cycle. The shared machinery adds no Settings callback or lifecycle coupling.
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
- **`WeightReconciler` declares its direct `set` dependency.** Direct file loads no longer rely on another framework entrypoint having already initialized the process-global `Set` constant.
|
|
10
|
+
- **Fleet execution-contract lookup preserves explicit `false`.** Symbol- and string-key envelope accessors now distinguish a present false marker from an absent marker, so malformed requests are rejected before provider or registry dispatch instead of silently downgrading to the legacy path.
|
|
11
|
+
|
|
12
|
+
## 0.7.5 - 2026-08-19
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- **Write-time lane weights on immutable Inventory records.** `Inventory::WeightSchema` computes the independent tier, provider, instance, and model-or-offering axes from current settings, preserving zero as an explicit disable and rejecting malformed values. `OfferingDraft`, `OfferingRecord`, and `LaneRecord` now carry an atomic validated weight pair; registry construction copies that frozen pair unchanged.
|
|
16
|
+
|
|
17
|
+
## 0.7.4 - 2026-08-19
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- **Authoritative Inventory lane-type mapping.** `Taxonomies::OPERATION_TO_LANE_TYPE` and `Taxonomies.lane_type_for` now own the complete canonical operation-to-lane-type mapping used by human-readable five-tuple lane identities. The deletion-scheduled coordinator adapter delegates to the same resolver instead of retaining a divergent compatibility copy.
|
|
21
|
+
|
|
3
22
|
## 0.7.3 - 2026-08-17
|
|
4
23
|
|
|
5
24
|
### Fixed
|
|
@@ -36,7 +36,10 @@ module Legion
|
|
|
36
36
|
|
|
37
37
|
FleetEnvelope = Struct.new(:data, keyword_init: true) do
|
|
38
38
|
def [](key)
|
|
39
|
-
|
|
39
|
+
symbol_key = key.to_sym
|
|
40
|
+
return data[symbol_key] if data.key?(symbol_key)
|
|
41
|
+
|
|
42
|
+
data[key.to_s]
|
|
40
43
|
end
|
|
41
44
|
|
|
42
45
|
def key?(key)
|
|
@@ -328,8 +328,12 @@ module Legion
|
|
|
328
328
|
|
|
329
329
|
def envelope_value(envelope, key)
|
|
330
330
|
return nil unless envelope.respond_to?(:key?)
|
|
331
|
+
return envelope[key] if envelope.key?(key)
|
|
331
332
|
|
|
332
|
-
|
|
333
|
+
string_key = key.to_s
|
|
334
|
+
return envelope[string_key] if envelope.key?(string_key)
|
|
335
|
+
|
|
336
|
+
nil
|
|
333
337
|
end
|
|
334
338
|
|
|
335
339
|
def normalize_hash(hash)
|
|
@@ -31,6 +31,35 @@ module Legion
|
|
|
31
31
|
|
|
32
32
|
PUBLICATION_SOURCES = %i[provider_catalog provider_static_catalog provider_control_plane].freeze
|
|
33
33
|
|
|
34
|
+
WEIGHT_INPUT_KEYS = %i[instance model_or_offering provider tier].freeze
|
|
35
|
+
IDENTITY_WEIGHT_INPUTS = {
|
|
36
|
+
tier: 100, provider: 100, instance: 100, model_or_offering: 100
|
|
37
|
+
}.freeze
|
|
38
|
+
IDENTITY_BASE_WEIGHT = 100_000_000
|
|
39
|
+
|
|
40
|
+
def validated_weight_pair(weight_inputs:, base_weight:)
|
|
41
|
+
raise Errors::ValidationError, 'weight_inputs and base_weight must be supplied together' \
|
|
42
|
+
if weight_inputs.nil? != base_weight.nil?
|
|
43
|
+
|
|
44
|
+
return [IDENTITY_WEIGHT_INPUTS, IDENTITY_BASE_WEIGHT] if weight_inputs.nil?
|
|
45
|
+
|
|
46
|
+
raise Errors::ValidationError, 'weight_inputs must be a Hash' \
|
|
47
|
+
unless weight_inputs.is_a?(::Hash)
|
|
48
|
+
unless weight_inputs.keys.length == WEIGHT_INPUT_KEYS.length &&
|
|
49
|
+
WEIGHT_INPUT_KEYS.all? { |key| weight_inputs.key?(key) }
|
|
50
|
+
raise Errors::ValidationError,
|
|
51
|
+
'weight_inputs must have keys tier/provider/instance/model_or_offering'
|
|
52
|
+
end
|
|
53
|
+
raise Errors::ValidationError, 'weight_inputs values must be Integers >= 0' \
|
|
54
|
+
unless weight_inputs.values.all? { |value| value.is_a?(::Integer) && value >= 0 }
|
|
55
|
+
raise Errors::ValidationError, 'base_weight must be an Integer >= 0' \
|
|
56
|
+
unless base_weight.is_a?(::Integer) && base_weight >= 0
|
|
57
|
+
raise Errors::ValidationError, 'base_weight must equal the product of weight_inputs' \
|
|
58
|
+
unless base_weight == weight_inputs.values.reduce(1, :*)
|
|
59
|
+
|
|
60
|
+
[weight_inputs.dup.freeze, base_weight]
|
|
61
|
+
end
|
|
62
|
+
|
|
34
63
|
def check_unknown_kwargs!(kwargs:, members:)
|
|
35
64
|
unknown = kwargs.keys - members
|
|
36
65
|
raise Errors::ValidationError, "unknown keyword(s): #{unknown.join(', ')}" unless unknown.empty?
|
|
@@ -251,17 +280,21 @@ module Legion
|
|
|
251
280
|
end
|
|
252
281
|
end
|
|
253
282
|
|
|
254
|
-
# An off-registry provider draft of one offering. Carries
|
|
255
|
-
#
|
|
256
|
-
# default model. See section 10.1.
|
|
283
|
+
# An off-registry provider draft of one offering. Carries the validated
|
|
284
|
+
# write-time weight pair, but no provider family, instance ID, offering ID,
|
|
285
|
+
# lane ID, callable, health, or default model. See section 10.1.
|
|
257
286
|
OfferingDraft = ::Data.define(
|
|
258
287
|
:provider_native_key, :model, :tier, :operation_evidence, :capability_evidence,
|
|
259
288
|
:context_evidence, :max_output_evidence, :embedding_dimensions_evidence,
|
|
260
|
-
:model_revision_evidence, :tokenizer_evidence, :quota_domains, :metadata, :publication_source
|
|
289
|
+
:model_revision_evidence, :tokenizer_evidence, :quota_domains, :metadata, :publication_source,
|
|
290
|
+
:weight_inputs, :base_weight
|
|
261
291
|
) do
|
|
262
292
|
def initialize(**kwargs)
|
|
263
293
|
kwargs = { capability_evidence: {}, quota_domains: {}, metadata: {} }.merge(kwargs)
|
|
264
294
|
RecordSupport.check_unknown_kwargs!(kwargs: kwargs, members: self.class.members)
|
|
295
|
+
weight_inputs, base_weight = RecordSupport.validated_weight_pair(
|
|
296
|
+
weight_inputs: kwargs[:weight_inputs], base_weight: kwargs[:base_weight]
|
|
297
|
+
)
|
|
265
298
|
|
|
266
299
|
super(
|
|
267
300
|
provider_native_key: Identity.normalize_text(value: kwargs[:provider_native_key], field: :provider_native_key),
|
|
@@ -272,21 +305,27 @@ module Legion
|
|
|
272
305
|
quota_domains: RecordSupport.validate_quota_domains!(kwargs[:quota_domains]),
|
|
273
306
|
metadata: RecordSupport.frozen_metadata(value: kwargs[:metadata]),
|
|
274
307
|
publication_source: RecordSupport.publication_source!(value: kwargs[:publication_source]),
|
|
308
|
+
weight_inputs: weight_inputs,
|
|
309
|
+
base_weight: base_weight,
|
|
275
310
|
**RecordSupport.scalar_value_evidences(kwargs)
|
|
276
311
|
)
|
|
277
312
|
end
|
|
278
313
|
end
|
|
279
314
|
|
|
280
|
-
# A registry-owned offering with canonical identity
|
|
281
|
-
#
|
|
315
|
+
# A registry-owned offering with canonical identity, captured callable, and
|
|
316
|
+
# the write-time weight pair copied unchanged from its draft. Only
|
|
317
|
+
# Inventory::Publisher/Registry constructs it. See section 10.2.
|
|
282
318
|
OfferingRecord = ::Data.define(
|
|
283
319
|
:offering_id, :provider_native_key, :instance_key, :model, :tier, :operation_evidence,
|
|
284
320
|
:capability_evidence, :context_evidence, :max_output_evidence, :embedding_dimensions_evidence,
|
|
285
321
|
:model_revision_evidence, :tokenizer_evidence, :quota_domains, :metadata, :callable_handle,
|
|
286
|
-
:publication_source
|
|
322
|
+
:publication_source, :weight_inputs, :base_weight
|
|
287
323
|
) do
|
|
288
324
|
def initialize(**kwargs)
|
|
289
325
|
RecordSupport.check_unknown_kwargs!(kwargs: kwargs, members: self.class.members)
|
|
326
|
+
weight_inputs, base_weight = RecordSupport.validated_weight_pair(
|
|
327
|
+
weight_inputs: kwargs[:weight_inputs], base_weight: kwargs[:base_weight]
|
|
328
|
+
)
|
|
290
329
|
instance_key = kwargs[:instance_key]
|
|
291
330
|
callable_handle = kwargs[:callable_handle]
|
|
292
331
|
raise Errors::ValidationError, 'instance_key must be an InstanceKey' unless instance_key.is_a?(Identity::InstanceKey)
|
|
@@ -307,6 +346,8 @@ module Legion
|
|
|
307
346
|
metadata: RecordSupport.frozen_metadata(value: kwargs[:metadata]),
|
|
308
347
|
callable_handle: callable_handle,
|
|
309
348
|
publication_source: RecordSupport.publication_source!(value: kwargs[:publication_source]),
|
|
349
|
+
weight_inputs: weight_inputs,
|
|
350
|
+
base_weight: base_weight,
|
|
310
351
|
**RecordSupport.scalar_value_evidences(kwargs)
|
|
311
352
|
)
|
|
312
353
|
end
|
|
@@ -342,15 +383,19 @@ module Legion
|
|
|
342
383
|
end
|
|
343
384
|
|
|
344
385
|
# An executable lane derived by the registry from one supported operation
|
|
345
|
-
# of an OfferingRecord
|
|
386
|
+
# of an OfferingRecord, with its write-time weight pair copied unchanged.
|
|
387
|
+
# See section 10.3.
|
|
346
388
|
LaneRecord = ::Data.define(
|
|
347
389
|
:lane_id, :offering_id, :instance_key, :provider_family, :instance_id, :model, :tier, :operation,
|
|
348
390
|
:capability_evidence, :context_evidence, :max_output_evidence, :embedding_dimensions_evidence,
|
|
349
391
|
:model_revision_evidence, :tokenizer_evidence, :quota_domain, :metadata, :callable_handle,
|
|
350
|
-
:publication_source
|
|
392
|
+
:publication_source, :weight_inputs, :base_weight
|
|
351
393
|
) do
|
|
352
394
|
def initialize(**kwargs)
|
|
353
395
|
RecordSupport.check_unknown_kwargs!(kwargs: kwargs, members: self.class.members)
|
|
396
|
+
weight_inputs, base_weight = RecordSupport.validated_weight_pair(
|
|
397
|
+
weight_inputs: kwargs[:weight_inputs], base_weight: kwargs[:base_weight]
|
|
398
|
+
)
|
|
354
399
|
instance_key = kwargs[:instance_key]
|
|
355
400
|
callable_handle = kwargs[:callable_handle]
|
|
356
401
|
raise Errors::ValidationError, 'instance_key must be an InstanceKey' unless instance_key.is_a?(Identity::InstanceKey)
|
|
@@ -383,6 +428,8 @@ module Legion
|
|
|
383
428
|
metadata: RecordSupport.frozen_metadata(value: kwargs[:metadata]),
|
|
384
429
|
callable_handle: callable_handle,
|
|
385
430
|
publication_source: RecordSupport.publication_source!(value: kwargs[:publication_source]),
|
|
431
|
+
weight_inputs: weight_inputs,
|
|
432
|
+
base_weight: base_weight,
|
|
386
433
|
**RecordSupport.scalar_value_evidences(kwargs)
|
|
387
434
|
)
|
|
388
435
|
end
|
|
@@ -491,7 +491,8 @@ module Legion
|
|
|
491
491
|
embedding_dimensions_evidence: draft.embedding_dimensions_evidence,
|
|
492
492
|
model_revision_evidence: draft.model_revision_evidence, tokenizer_evidence: draft.tokenizer_evidence,
|
|
493
493
|
quota_domains: draft.quota_domains, metadata: draft.metadata, callable_handle: handle,
|
|
494
|
-
publication_source: draft.publication_source
|
|
494
|
+
publication_source: draft.publication_source,
|
|
495
|
+
weight_inputs: draft.weight_inputs, base_weight: draft.base_weight
|
|
495
496
|
)
|
|
496
497
|
end
|
|
497
498
|
|
|
@@ -520,7 +521,8 @@ module Legion
|
|
|
520
521
|
embedding_dimensions_evidence: offering.embedding_dimensions_evidence,
|
|
521
522
|
model_revision_evidence: offering.model_revision_evidence, tokenizer_evidence: offering.tokenizer_evidence,
|
|
522
523
|
quota_domain: offering.quota_domain(operation: operation), metadata: offering.metadata,
|
|
523
|
-
callable_handle: handle, publication_source: offering.publication_source
|
|
524
|
+
callable_handle: handle, publication_source: offering.publication_source,
|
|
525
|
+
weight_inputs: offering.weight_inputs, base_weight: offering.base_weight
|
|
524
526
|
)
|
|
525
527
|
end
|
|
526
528
|
|
|
@@ -121,7 +121,6 @@ module Legion
|
|
|
121
121
|
include Legion::Logging::Helper
|
|
122
122
|
|
|
123
123
|
HEALTHY_COMPAT = { circuit_state: :closed, denied: false, available: true, adjustment: 0 }.freeze
|
|
124
|
-
LEGACY_TYPES = { embed: :embedding, image: :image, transcribe: :audio, translate: :audio, speak: :audio }.freeze
|
|
125
124
|
LEGACY_CAPABILITIES = {
|
|
126
125
|
chat: :completion, stream_chat: :streaming, embed: :embedding, image: :image,
|
|
127
126
|
transcribe: :audio_transcription, translate: :audio_transcription, speak: :audio_speech, moderate: :moderation
|
|
@@ -174,7 +173,7 @@ module Legion
|
|
|
174
173
|
def group_lanes(snapshot, instance_key)
|
|
175
174
|
groups = {}
|
|
176
175
|
snapshot.lanes_for(instance_key: instance_key).each do |lane|
|
|
177
|
-
type =
|
|
176
|
+
type = Taxonomies.lane_type_for(operation: lane.operation)
|
|
178
177
|
key = [lane.tier, instance_key.provider_family, instance_key.instance_id, type, lane.model]
|
|
179
178
|
group = (groups[key] ||= new_group(instance_key, lane, type))
|
|
180
179
|
accumulate_group(group, lane)
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Direct dependency; the explicit receiver keeps Ruby 4's redundant-require cop from deleting it.
|
|
4
|
+
Kernel.require 'set'
|
|
5
|
+
require 'legion/extensions/llm/inventory/weight_schema'
|
|
6
|
+
require 'legion/extensions/llm/settings_cascade'
|
|
7
|
+
require 'legion/extensions/llm/inventory/identity'
|
|
8
|
+
|
|
9
|
+
module Legion
|
|
10
|
+
module Extensions
|
|
11
|
+
module Llm
|
|
12
|
+
module Inventory
|
|
13
|
+
# Tracks configured weight keys that currently have no published lane.
|
|
14
|
+
class DormantWeightTracker
|
|
15
|
+
def initialize
|
|
16
|
+
@dormant = Set.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def observe(configured_keys:, published_keys:)
|
|
20
|
+
current = Set.new(configured_keys).difference(published_keys)
|
|
21
|
+
newly_dormant = current.difference(@dormant).sort_by(&:inspect)
|
|
22
|
+
@dormant.replace(current)
|
|
23
|
+
newly_dormant
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def clear!
|
|
27
|
+
@dormant.clear
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Shared atomic write-time weight publication for existing writer cadences.
|
|
32
|
+
module WeightReconciler
|
|
33
|
+
module_function
|
|
34
|
+
|
|
35
|
+
def rebuild_offerings(settings:, instance_key:, offerings:)
|
|
36
|
+
offerings.map do |draft|
|
|
37
|
+
inputs = WeightSchema.weight_inputs(
|
|
38
|
+
settings: settings,
|
|
39
|
+
instance_key: instance_key,
|
|
40
|
+
provider_native_key: draft.provider_native_key,
|
|
41
|
+
model: draft.model,
|
|
42
|
+
tier: draft.tier
|
|
43
|
+
)
|
|
44
|
+
draft.with(weight_inputs: inputs, base_weight: WeightSchema.base_weight(inputs))
|
|
45
|
+
end.freeze
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Periodic discovery calls this AFTER its existing network/discovery work. Read
|
|
49
|
+
# current Settings and perform weight rebuilding, comparison, sequence
|
|
50
|
+
# allocation, publish, and cache update under the same writer mutex. No Settings
|
|
51
|
+
# callback or separate reweight path exists.
|
|
52
|
+
def commit_if_changed!(settings:, instance_id:, state:, discovered_offerings:, **publication)
|
|
53
|
+
unknown = publication.keys - %i[mutex equivalent replace stable_signature]
|
|
54
|
+
raise ArgumentError, "unknown publication keyword(s): #{unknown.join(', ')}" unless unknown.empty?
|
|
55
|
+
|
|
56
|
+
mutex = publication.fetch(:mutex)
|
|
57
|
+
equivalent = publication.fetch(:equivalent)
|
|
58
|
+
replace = publication.fetch(:replace)
|
|
59
|
+
stable_signature = publication[:stable_signature]
|
|
60
|
+
mutex.synchronize do
|
|
61
|
+
offerings = rebuild_offerings(
|
|
62
|
+
settings: settings, instance_key: state.fetch(:instance_key),
|
|
63
|
+
offerings: discovered_offerings
|
|
64
|
+
)
|
|
65
|
+
return false if equivalent.call(state.fetch(:offerings), offerings)
|
|
66
|
+
|
|
67
|
+
if state.fetch(:published)
|
|
68
|
+
commit_locked!(
|
|
69
|
+
instance_id: instance_id, state: state, offerings: offerings,
|
|
70
|
+
replace: replace, stable_signature: stable_signature
|
|
71
|
+
)
|
|
72
|
+
else
|
|
73
|
+
cache_locked!(
|
|
74
|
+
state: state, offerings: offerings, stable_signature: stable_signature
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
true
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Track a claimed but not-yet-activated instance before its readiness I/O. An
|
|
82
|
+
# ordinary writer pass may refresh its cached weights, but must never send a
|
|
83
|
+
# replacement for a publication that is still :initializing.
|
|
84
|
+
def track_initializing!(states:, state_key:, state:, mutex:)
|
|
85
|
+
mutex.synchronize do
|
|
86
|
+
state[:published] = false
|
|
87
|
+
states[state_key] = state
|
|
88
|
+
end
|
|
89
|
+
state
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Initial and recovery activation both use this helper after readiness succeeds.
|
|
93
|
+
# Rebuild from current settings and publish under the writer's publication
|
|
94
|
+
# mutex, then mark the state published only after the publisher call succeeds.
|
|
95
|
+
def activate_tracked!(settings:, instance_id:, state_key:, state:, **activation)
|
|
96
|
+
unknown = activation.keys - %i[states mutex probe_token activate activation_sequence stable_signature]
|
|
97
|
+
raise ArgumentError, "unknown activation keyword(s): #{unknown.join(', ')}" unless unknown.empty?
|
|
98
|
+
|
|
99
|
+
states = activation.fetch(:states)
|
|
100
|
+
mutex = activation.fetch(:mutex)
|
|
101
|
+
probe_token = activation.fetch(:probe_token)
|
|
102
|
+
activate = activation.fetch(:activate)
|
|
103
|
+
activation_sequence = activation.fetch(:activation_sequence)
|
|
104
|
+
stable_signature = activation[:stable_signature]
|
|
105
|
+
mutex.synchronize do
|
|
106
|
+
return false unless states[state_key].equal?(state)
|
|
107
|
+
|
|
108
|
+
offerings = rebuild_offerings(
|
|
109
|
+
settings: settings, instance_key: state.fetch(:instance_key),
|
|
110
|
+
offerings: state.fetch(:offerings)
|
|
111
|
+
)
|
|
112
|
+
sequence = activation_sequence.call(state)
|
|
113
|
+
activate.call(
|
|
114
|
+
instance_id: instance_id, state: state, offerings: offerings,
|
|
115
|
+
sequence: sequence, probe_token: probe_token
|
|
116
|
+
)
|
|
117
|
+
state[:sequence] = sequence
|
|
118
|
+
cache_locked!(
|
|
119
|
+
state: state, offerings: offerings, stable_signature: stable_signature
|
|
120
|
+
)
|
|
121
|
+
state[:published] = true
|
|
122
|
+
true
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Ordinary ticks call this after reconcile. Dormant observation has the same
|
|
127
|
+
# cadence as the existing writer; there is no reload callback path.
|
|
128
|
+
def observe_dormant!(settings:, provider_family:, states:, mutex:, **observation)
|
|
129
|
+
unknown = observation.keys - %i[tracker dormant_logger]
|
|
130
|
+
raise ArgumentError, "unknown observation keyword(s): #{unknown.join(', ')}" unless unknown.empty?
|
|
131
|
+
|
|
132
|
+
tracker = observation.fetch(:tracker)
|
|
133
|
+
dormant_logger = observation.fetch(:dormant_logger)
|
|
134
|
+
mutex.synchronize do
|
|
135
|
+
observe_dormant_locked!(
|
|
136
|
+
settings: settings, provider_family: provider_family, states: states,
|
|
137
|
+
tracker: tracker, dormant_logger: dormant_logger
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def commit_locked!(instance_id:, state:, offerings:, replace:, stable_signature:)
|
|
143
|
+
sequence = state.fetch(:sequence) + 1
|
|
144
|
+
replace.call(instance_id: instance_id, state: state, offerings: offerings, sequence: sequence)
|
|
145
|
+
state[:sequence] = sequence
|
|
146
|
+
cache_locked!(
|
|
147
|
+
state: state, offerings: offerings, stable_signature: stable_signature
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
private_class_method :commit_locked!
|
|
151
|
+
|
|
152
|
+
def cache_locked!(state:, offerings:, stable_signature:)
|
|
153
|
+
state[:offerings] = offerings
|
|
154
|
+
state[:signature] = stable_signature.call(offerings) if stable_signature
|
|
155
|
+
end
|
|
156
|
+
private_class_method :cache_locked!
|
|
157
|
+
|
|
158
|
+
def observe_dormant_locked!(settings:, provider_family:, states:, tracker:,
|
|
159
|
+
dormant_logger:)
|
|
160
|
+
configured = configured_weight_keys(settings: settings, provider_family: provider_family)
|
|
161
|
+
published = published_weight_keys(provider_family: provider_family, states: states)
|
|
162
|
+
tracker.observe(configured_keys: configured, published_keys: published).each do |key|
|
|
163
|
+
dormant_logger.call(key)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
private_class_method :observe_dormant_locked!
|
|
167
|
+
|
|
168
|
+
def configured_weight_keys(settings:, provider_family:)
|
|
169
|
+
llm = config_hash(settings.dig(:extensions, :llm))
|
|
170
|
+
provider = config_hash(SettingsCascade.lookup(llm, provider_family))
|
|
171
|
+
keys = Set.new
|
|
172
|
+
keys << canonical_key(provider_family, :provider) if weight_present?(provider)
|
|
173
|
+
|
|
174
|
+
config_hash(SettingsCascade.lookup(provider, :models)).each do |model, config|
|
|
175
|
+
keys << canonical_key(provider_family, :model, model.to_s) if weight_present?(config_hash(config))
|
|
176
|
+
end
|
|
177
|
+
config_hash(SettingsCascade.lookup(provider, :instances)).each do |instance, config|
|
|
178
|
+
instance_config = config_hash(config)
|
|
179
|
+
keys << canonical_key(provider_family, :instance, instance.to_s) if weight_present?(instance_config)
|
|
180
|
+
config_hash(SettingsCascade.lookup(instance_config, :models)).each do |model, model_config|
|
|
181
|
+
next unless weight_present?(config_hash(model_config))
|
|
182
|
+
|
|
183
|
+
keys << canonical_key(
|
|
184
|
+
provider_family, :instance, instance.to_s, :model, model.to_s
|
|
185
|
+
)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
config_hash(SettingsCascade.lookup(provider, :offerings)).each do |offering_id, config|
|
|
189
|
+
keys << canonical_key(provider_family, :offering, offering_id.to_s) \
|
|
190
|
+
if weight_present?(config_hash(config))
|
|
191
|
+
end
|
|
192
|
+
keys
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def published_weight_keys(provider_family:, states:)
|
|
196
|
+
keys = Set.new
|
|
197
|
+
states.each_value do |state|
|
|
198
|
+
next unless state.fetch(:published)
|
|
199
|
+
|
|
200
|
+
offerings = Array(state[:offerings])
|
|
201
|
+
next if offerings.empty?
|
|
202
|
+
|
|
203
|
+
instance_key = state.fetch(:instance_key)
|
|
204
|
+
keys << canonical_key(provider_family, :provider)
|
|
205
|
+
keys << canonical_key(provider_family, :instance, instance_key.instance_id.to_s)
|
|
206
|
+
offerings.each do |draft|
|
|
207
|
+
keys << canonical_key(provider_family, :model, draft.model.to_s)
|
|
208
|
+
keys << canonical_key(
|
|
209
|
+
provider_family, :instance, instance_key.instance_id.to_s,
|
|
210
|
+
:model, draft.model.to_s
|
|
211
|
+
)
|
|
212
|
+
offering_id = Identity.offering_id(
|
|
213
|
+
instance_key: instance_key, provider_native_key: draft.provider_native_key
|
|
214
|
+
)
|
|
215
|
+
keys << canonical_key(provider_family, :offering, offering_id.to_s)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
keys
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def canonical_key(provider_family, *parts)
|
|
222
|
+
[provider_family.to_sym, *parts].freeze
|
|
223
|
+
end
|
|
224
|
+
private_class_method :canonical_key
|
|
225
|
+
|
|
226
|
+
def config_hash(value)
|
|
227
|
+
return {} if value.nil?
|
|
228
|
+
return value if value.is_a?(::Hash)
|
|
229
|
+
|
|
230
|
+
raise ArgumentError, "weight configuration scope must be a Hash, got #{value.inspect}"
|
|
231
|
+
end
|
|
232
|
+
private_class_method :config_hash
|
|
233
|
+
|
|
234
|
+
def weight_present?(scope)
|
|
235
|
+
!SettingsCascade.lookup(scope, :weight).nil?
|
|
236
|
+
end
|
|
237
|
+
private_class_method :weight_present?
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/llm/settings_cascade'
|
|
4
|
+
require 'legion/extensions/llm/inventory/identity'
|
|
5
|
+
|
|
6
|
+
module Legion
|
|
7
|
+
module Extensions
|
|
8
|
+
module Llm
|
|
9
|
+
module Inventory
|
|
10
|
+
# Write-time lane-weight computation (RANKING v2 law): the stateless router
|
|
11
|
+
# reads the stored scalar; it is computed ONLY here, at write events, from a
|
|
12
|
+
# live settings read. Each multiplicative axis reads ITS OWN scope — never a
|
|
13
|
+
# fall-through cascade (a fall-through would double-count the other axes).
|
|
14
|
+
module WeightSchema
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
IDENTITY = 100
|
|
18
|
+
|
|
19
|
+
# Exact 4-key hash. offering_id derives from instance_key + the REAL
|
|
20
|
+
# provider native key (NOT the model — azure's native key is
|
|
21
|
+
# deployment_name, which can differ from model_id).
|
|
22
|
+
def weight_inputs(settings:, instance_key:, provider_native_key:, model:, tier:)
|
|
23
|
+
llm_conf = scope_hash(settings.dig(:extensions, :llm), path: 'extensions.llm')
|
|
24
|
+
provider_conf = scope_hash(
|
|
25
|
+
SettingsCascade.lookup(llm_conf, instance_key.provider_family),
|
|
26
|
+
path: "extensions.llm.#{instance_key.provider_family}"
|
|
27
|
+
)
|
|
28
|
+
instances = scope_hash(
|
|
29
|
+
SettingsCascade.lookup(provider_conf, :instances), path: 'provider.instances'
|
|
30
|
+
)
|
|
31
|
+
instance_cfg = scope_hash(
|
|
32
|
+
SettingsCascade.lookup(instances, instance_key.instance_id),
|
|
33
|
+
path: "provider.instances.#{instance_key.instance_id}"
|
|
34
|
+
)
|
|
35
|
+
tier_weights = scope_hash(
|
|
36
|
+
settings.dig(:llm, :routing, :tier_weights), path: 'llm.routing.tier_weights'
|
|
37
|
+
)
|
|
38
|
+
offering_id = Identity.offering_id(instance_key: instance_key, provider_native_key: provider_native_key)
|
|
39
|
+
offerings = scope_hash(SettingsCascade.lookup(provider_conf, :offerings), path: 'provider.offerings')
|
|
40
|
+
offering_conf = scope_hash(
|
|
41
|
+
SettingsCascade.lookup(offerings, offering_id), path: "provider.offerings.#{offering_id}"
|
|
42
|
+
)
|
|
43
|
+
provider_models = scope_hash(
|
|
44
|
+
SettingsCascade.lookup(provider_conf, :models), path: 'provider.models'
|
|
45
|
+
)
|
|
46
|
+
instance_models = scope_hash(
|
|
47
|
+
SettingsCascade.lookup(instance_cfg, :models), path: 'instance.models'
|
|
48
|
+
)
|
|
49
|
+
scope_hash(
|
|
50
|
+
SettingsCascade.lookup(provider_models, model), path: "provider.models.#{model}"
|
|
51
|
+
)
|
|
52
|
+
scope_hash(
|
|
53
|
+
SettingsCascade.lookup(instance_models, model), path: "instance.models.#{model}"
|
|
54
|
+
)
|
|
55
|
+
model_scopes = scope_hash(
|
|
56
|
+
SettingsCascade.merge_model_scopes(
|
|
57
|
+
provider_conf: provider_conf, instance_cfg: instance_cfg, model: model
|
|
58
|
+
),
|
|
59
|
+
path: "merged model scope #{model}"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
offering_w = SettingsCascade.lookup(offering_conf, :weight)
|
|
63
|
+
model_w = SettingsCascade.lookup(model_scopes, :weight)
|
|
64
|
+
{
|
|
65
|
+
tier: component(SettingsCascade.lookup(tier_weights, tier), IDENTITY),
|
|
66
|
+
provider: component(SettingsCascade.lookup(provider_conf, :weight), IDENTITY),
|
|
67
|
+
instance: component(SettingsCascade.lookup(instance_cfg, :weight), IDENTITY),
|
|
68
|
+
# Explicit nil? precedence (NEVER `||`): offering overrides the model
|
|
69
|
+
# component only; nil offering → model; both nil → identity. A
|
|
70
|
+
# non-nil non-Integer (false, strings, negatives) RAISES in `component`.
|
|
71
|
+
model_or_offering: component(offering_w.nil? ? model_w : offering_w, IDENTITY)
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# nil → default; an explicit 0 passes through (0 = operator disable —
|
|
76
|
+
# 0 is TRUTHY in Ruby and must not be defaulted); any other non-Integer
|
|
77
|
+
# RAISES instead of silently applying a different weight.
|
|
78
|
+
def component(value, default)
|
|
79
|
+
return default if value.nil?
|
|
80
|
+
|
|
81
|
+
raise ArgumentError, "weight component must be an Integer >= 0, got #{value.inspect}" \
|
|
82
|
+
unless value.is_a?(::Integer) && value >= 0
|
|
83
|
+
|
|
84
|
+
value
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Missing scope is the identity default. A present malformed scope is
|
|
88
|
+
# never treated as missing (`false || {}` would silently erase operator
|
|
89
|
+
# input and recreate a flat identity result).
|
|
90
|
+
def scope_hash(value, path:)
|
|
91
|
+
return {} if value.nil?
|
|
92
|
+
return value if value.is_a?(::Hash)
|
|
93
|
+
|
|
94
|
+
raise ArgumentError, "#{path} must be a Hash, got #{value.inspect}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Pinned return contract: TWO methods. The writer calls both:
|
|
98
|
+
# wi = WeightSchema.weight_inputs(...)
|
|
99
|
+
# base = WeightSchema.base_weight(wi)
|
|
100
|
+
def base_weight(weight_inputs)
|
|
101
|
+
weight_inputs.values.reduce(1, :*)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -11,6 +11,25 @@ module Legion
|
|
|
11
11
|
module Taxonomies
|
|
12
12
|
TIERS = %i[direct local fleet cloud frontier].freeze
|
|
13
13
|
TYPES = %i[inference embedding image audio].freeze
|
|
14
|
+
|
|
15
|
+
# The current authoritative operation → Inventory lane-type mapping (the 4th
|
|
16
|
+
# field of the 5-tuple lane id). NON-LEGACY home: the deletion-scheduled
|
|
17
|
+
# LegacyCoordinatorAdapter consumes `lane_type_for` below until deletion;
|
|
18
|
+
# it must not retain a second mapping. moderate/count_tokens
|
|
19
|
+
# are non-generative inference operations — no moderation/count type exists in
|
|
20
|
+
# TYPES, so they map to :inference.
|
|
21
|
+
OPERATION_TO_LANE_TYPE = {
|
|
22
|
+
chat: :inference,
|
|
23
|
+
stream_chat: :inference,
|
|
24
|
+
embed: :embedding,
|
|
25
|
+
image: :image,
|
|
26
|
+
transcribe: :audio,
|
|
27
|
+
translate: :audio,
|
|
28
|
+
speak: :audio,
|
|
29
|
+
moderate: :inference,
|
|
30
|
+
count_tokens: :inference
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
14
33
|
CIRCUIT_STATES = %i[closed half_open open].freeze
|
|
15
34
|
HEALTH_KEYS = %i[circuit_state denied available adjustment].freeze
|
|
16
35
|
|
|
@@ -79,6 +98,11 @@ module Legion
|
|
|
79
98
|
|
|
80
99
|
module_function
|
|
81
100
|
|
|
101
|
+
def lane_type_for(operation:)
|
|
102
|
+
canonical = normalize_operation(value: operation, allow_aliases: false)
|
|
103
|
+
OPERATION_TO_LANE_TYPE.fetch(canonical)
|
|
104
|
+
end
|
|
105
|
+
|
|
82
106
|
# Normalize an operation to one of OPERATIONS. Ingress and protocol-v2
|
|
83
107
|
# compatibility adapters pass allow_aliases: true; registry records and
|
|
84
108
|
# exact-execution envelopes pass false. Unknown, empty, or invalid UTF-8
|
|
@@ -52,6 +52,8 @@ module Legion
|
|
|
52
52
|
require_relative 'llm/inventory/errors'
|
|
53
53
|
require_relative 'llm/inventory/immutable_value'
|
|
54
54
|
require_relative 'llm/inventory/identity'
|
|
55
|
+
require_relative 'llm/inventory/weight_schema'
|
|
56
|
+
require_relative 'llm/inventory/weight_reconciler'
|
|
55
57
|
require_relative 'llm/inventory/evidence'
|
|
56
58
|
require_relative 'llm/inventory/callable_handle'
|
|
57
59
|
require_relative 'llm/inventory/probe_token'
|