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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e3a6411175e21627e4ebe35751eff6e1f202cb9ed82ad8ffd9d37cae6311849d
4
- data.tar.gz: b19ea34c0104010f2bd63db81729e123b79dfd3cb56b61ad5967cd03e54890d5
3
+ metadata.gz: b11032eff9bab4fc8c4ad457cd78fdce95f6ec245a5938dbd6ea987729db4238
4
+ data.tar.gz: f799901fe6f05201a25237aeafb7779538a995e63e5117e18550935b9e77e12e
5
5
  SHA512:
6
- metadata.gz: 9c913c74bff87677f3d9dd0eaa6bad854a74c5d54794aebecf8371b9791166783fc35c4d33a459c1caf9314cf16674e05a7b11ef426b29df1b275465bd32c939
7
- data.tar.gz: 7a7dd7e3b3414031bfd6807732c8bcf14e03075c0b020d61ba8621cf9caa32d08be5951b72a995c70ba6042778d1ef732b1b053b92aa0c3813a5636e7f265095
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
- data[key.to_sym] || data[key.to_s]
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
- envelope[key] || envelope[key.to_s]
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 no provider
255
- # family, instance ID, offering ID, lane ID, callable, health, weight, or
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 and captured callable.
281
- # Only Inventory::Publisher/Registry constructs it. See section 10.2.
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. See section 10.3.
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 = LEGACY_TYPES.fetch(lane.operation, :inference)
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
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Llm
6
- VERSION = '0.7.3'
6
+ VERSION = '0.7.6'
7
7
  end
8
8
  end
9
9
  end
@@ -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'